feat: allow saving multiple attachments (#528)
* feat: allow saving multiple attachments Refs: https://github.com/FossifyOrg/Messages/issues/75 * docs: update changelog * docs: update changelog * fix: move work to the background thread * feat: allow saving attachment selections from different messages Bonus for https://github.com/FossifyOrg/Messages/issues/75
This commit is contained in:
parent
3fb86af731
commit
4c96bb2056
6 changed files with 91 additions and 47 deletions
|
|
@ -5,6 +5,8 @@ import android.app.Activity
|
|||
import android.app.AlarmManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
|
|
@ -12,6 +14,7 @@ import android.media.MediaMetadataRetriever
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Telephony
|
||||
import android.provider.Telephony.Sms.MESSAGE_TYPE_QUEUED
|
||||
|
|
@ -44,6 +47,7 @@ import androidx.core.view.WindowCompat
|
|||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
|
|
@ -62,6 +66,7 @@ import org.fossify.commons.extensions.darkenColor
|
|||
import org.fossify.commons.extensions.formatDate
|
||||
import org.fossify.commons.extensions.getBottomNavigationBackgroundColor
|
||||
import org.fossify.commons.extensions.getContrastColor
|
||||
import org.fossify.commons.extensions.getFilenameFromPath
|
||||
import org.fossify.commons.extensions.getFilenameFromUri
|
||||
import org.fossify.commons.extensions.getMyContactsCursor
|
||||
import org.fossify.commons.extensions.getMyFileUri
|
||||
|
|
@ -113,6 +118,7 @@ import org.fossify.messages.dialogs.ScheduleMessageDialog
|
|||
import org.fossify.messages.extensions.clearExpiredScheduledMessages
|
||||
import org.fossify.messages.extensions.config
|
||||
import org.fossify.messages.extensions.conversationsDB
|
||||
import org.fossify.messages.extensions.copyToUri
|
||||
import org.fossify.messages.extensions.createTemporaryThread
|
||||
import org.fossify.messages.extensions.deleteConversation
|
||||
import org.fossify.messages.extensions.deleteMessage
|
||||
|
|
@ -158,6 +164,7 @@ import org.fossify.messages.helpers.MESSAGES_LIMIT
|
|||
import org.fossify.messages.helpers.PICK_CONTACT_INTENT
|
||||
import org.fossify.messages.helpers.PICK_DOCUMENT_INTENT
|
||||
import org.fossify.messages.helpers.PICK_PHOTO_INTENT
|
||||
import org.fossify.messages.helpers.PICK_SAVE_DIR_INTENT
|
||||
import org.fossify.messages.helpers.PICK_SAVE_FILE_INTENT
|
||||
import org.fossify.messages.helpers.PICK_VIDEO_INTENT
|
||||
import org.fossify.messages.helpers.SEARCHED_MESSAGE_ID
|
||||
|
|
@ -192,8 +199,6 @@ import org.greenrobot.eventbus.Subscribe
|
|||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.joda.time.DateTime
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class ThreadActivity : SimpleActivity() {
|
||||
private val MIN_DATE_TIME_DIFF_SECS = 300
|
||||
|
|
@ -215,7 +220,7 @@ class ThreadActivity : SimpleActivity() {
|
|||
private var privateContacts = ArrayList<SimpleContact>()
|
||||
private var messages = ArrayList<Message>()
|
||||
private val availableSIMCards = ArrayList<SIMCard>()
|
||||
private var lastAttachmentUri: String? = null
|
||||
private var pendingAttachmentsToSave: List<Attachment>? = null
|
||||
private var capturedImageUri: Uri? = null
|
||||
private var loadingOlderMessages = false
|
||||
private var allMessagesFetched = false
|
||||
|
|
@ -420,7 +425,8 @@ class ThreadActivity : SimpleActivity() {
|
|||
PICK_VIDEO_INTENT -> addAttachment(data)
|
||||
|
||||
PICK_CONTACT_INTENT -> addContactAttachment(data)
|
||||
PICK_SAVE_FILE_INTENT -> saveAttachment(resultData)
|
||||
PICK_SAVE_FILE_INTENT -> saveAttachments(resultData)
|
||||
PICK_SAVE_DIR_INTENT -> saveAttachments(resultData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1485,29 +1491,35 @@ class ThreadActivity : SimpleActivity() {
|
|||
checkSendMessageAvailability()
|
||||
}
|
||||
|
||||
private fun saveAttachment(resultData: Intent) {
|
||||
val takeFlags =
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
private fun saveAttachments(resultData: Intent) {
|
||||
applicationContext.contentResolver.takePersistableUriPermission(
|
||||
resultData.data!!,
|
||||
takeFlags
|
||||
resultData.data!!, FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(Uri.parse(lastAttachmentUri))
|
||||
outputStream =
|
||||
contentResolver.openOutputStream(Uri.parse(resultData.dataString!!), "rwt")
|
||||
inputStream!!.copyTo(outputStream!!)
|
||||
outputStream.flush()
|
||||
toast(org.fossify.commons.R.string.file_saved)
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
val destinationUri = resultData.data ?: return
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
if (DocumentsContract.isTreeUri(destinationUri)) {
|
||||
val outputDir = DocumentFile.fromTreeUri(this, destinationUri)
|
||||
?: return@ensureBackgroundThread
|
||||
pendingAttachmentsToSave?.forEach { attachment ->
|
||||
val documentFile = outputDir.createFile(
|
||||
attachment.mimetype,
|
||||
attachment.filename.takeIf { it.isNotBlank() }
|
||||
?: attachment.uriString.getFilenameFromPath()
|
||||
) ?: return@forEach
|
||||
copyToUri(src = attachment.getUri(), dst = documentFile.uri)
|
||||
}
|
||||
} else {
|
||||
copyToUri(pendingAttachmentsToSave!!.first().getUri(), resultData.data!!)
|
||||
}
|
||||
|
||||
toast(org.fossify.commons.R.string.file_saved)
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
} finally {
|
||||
pendingAttachmentsToSave = null
|
||||
}
|
||||
}
|
||||
lastAttachmentUri = null
|
||||
}
|
||||
|
||||
private fun checkSendMessageAvailability() {
|
||||
|
|
@ -1745,18 +1757,29 @@ class ThreadActivity : SimpleActivity() {
|
|||
return participants
|
||||
}
|
||||
|
||||
fun saveMMS(mimeType: String, path: String) {
|
||||
hideKeyboard()
|
||||
lastAttachmentUri = path
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = mimeType
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_TITLE, path.split("/").last())
|
||||
launchActivityForResult(
|
||||
intent = this,
|
||||
requestCode = PICK_SAVE_FILE_INTENT,
|
||||
error = org.fossify.commons.R.string.system_service_disabled
|
||||
)
|
||||
fun saveMMS(attachments: List<Attachment>) {
|
||||
pendingAttachmentsToSave = attachments
|
||||
if (attachments.size == 1) {
|
||||
val attachment = attachments.first()
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = attachment.mimetype
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_TITLE, attachment.uriString.split("/").last())
|
||||
launchActivityForResult(
|
||||
intent = this,
|
||||
requestCode = PICK_SAVE_FILE_INTENT,
|
||||
error = org.fossify.commons.R.string.system_service_disabled
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
addCategory(Intent.CATEGORY_DEFAULT)
|
||||
launchActivityForResult(
|
||||
intent = this,
|
||||
requestCode = PICK_SAVE_DIR_INTENT,
|
||||
error = org.fossify.commons.R.string.system_service_disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import android.annotation.SuppressLint
|
|||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Size
|
||||
import android.util.TypedValue
|
||||
|
|
@ -49,6 +47,7 @@ import org.fossify.messages.models.Attachment
|
|||
import org.fossify.messages.models.Message
|
||||
import org.fossify.messages.models.ThreadItem
|
||||
import org.fossify.messages.models.ThreadItem.*
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
|
||||
class ThreadAdapter(
|
||||
activity: SimpleActivity,
|
||||
|
|
@ -74,9 +73,13 @@ class ThreadAdapter(
|
|||
val isOneItemSelected = isOneItemSelected()
|
||||
val selectedItem = getSelectedItems().firstOrNull() as? Message
|
||||
val hasText = selectedItem?.body != null && selectedItem.body != ""
|
||||
val showSaveAs = getSelectedItems().all {
|
||||
it is Message && (it.attachment?.attachments?.size ?: 0) > 0
|
||||
} && getSelectedAttachments().isNotEmpty()
|
||||
|
||||
menu.apply {
|
||||
findItem(R.id.cab_copy_to_clipboard).isVisible = isOneItemSelected && hasText
|
||||
findItem(R.id.cab_save_as).isVisible = isOneItemSelected && selectedItem?.attachment?.attachments?.size == 1
|
||||
findItem(R.id.cab_save_as).isVisible = showSaveAs
|
||||
findItem(R.id.cab_share).isVisible = isOneItemSelected && hasText
|
||||
findItem(R.id.cab_forward_message).isVisible = isOneItemSelected
|
||||
findItem(R.id.cab_select_text).isVisible = isOneItemSelected && hasText
|
||||
|
|
@ -168,10 +171,16 @@ class ThreadAdapter(
|
|||
activity.copyToClipboard(firstItem.body)
|
||||
}
|
||||
|
||||
private fun getSelectedAttachments(): List<Attachment> {
|
||||
val selectedMessages = getSelectedItems().filterIsInstance<Message>()
|
||||
return selectedMessages.flatMap { it.attachment?.attachments.orEmpty() }
|
||||
}
|
||||
|
||||
private fun saveAs() {
|
||||
val firstItem = getSelectedItems().firstOrNull() as? Message ?: return
|
||||
val attachment = firstItem.attachment?.attachments?.first() ?: return
|
||||
(activity as ThreadActivity).saveMMS(attachment.mimetype, attachment.uriString)
|
||||
val attachments = getSelectedAttachments()
|
||||
if (attachments.isNotEmpty()) {
|
||||
(activity as ThreadActivity).saveMMS(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareText() {
|
||||
|
|
@ -342,7 +351,7 @@ class ThreadAdapter(
|
|||
|
||||
if (!activity.isFinishing && !activity.isDestroyed) {
|
||||
val contactLetterIcon = SimpleContactsHelper(activity).getContactLetterIcon(message.senderName)
|
||||
val placeholder = BitmapDrawable(activity.resources, contactLetterIcon)
|
||||
val placeholder = contactLetterIcon.toDrawable(activity.resources)
|
||||
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
|
|
@ -406,7 +415,7 @@ class ThreadAdapter(
|
|||
val imageView = ItemAttachmentImageBinding.inflate(layoutInflater)
|
||||
threadMessageAttachmentsHolder.addView(imageView.root)
|
||||
|
||||
val placeholderDrawable = ColorDrawable(Color.TRANSPARENT)
|
||||
val placeholderDrawable = Color.TRANSPARENT.toDrawable()
|
||||
val isTallImage = attachment.height > attachment.width
|
||||
val transformation = if (isTallImage) CenterCrop() else FitCenter()
|
||||
val options = RequestOptions()
|
||||
|
|
@ -442,7 +451,7 @@ class ThreadAdapter(
|
|||
|
||||
try {
|
||||
builder.into(imageView.attachmentImage)
|
||||
} catch (ignore: Exception) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
imageView.attachmentImage.setOnClickListener {
|
||||
|
|
|
|||
|
|
@ -1317,3 +1317,11 @@ fun Context.getDefaultKeyboardHeight(): Int {
|
|||
fun Context.shouldUnarchive(): Boolean {
|
||||
return config.isArchiveAvailable && !config.keepConversationsArchived
|
||||
}
|
||||
|
||||
fun Context.copyToUri(src: Uri, dst: Uri) {
|
||||
contentResolver.openInputStream(src)?.use { input ->
|
||||
contentResolver.openOutputStream(dst, "rwt")?.use { out ->
|
||||
input.copyTo(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ const val CAPTURE_VIDEO_INTENT = 45
|
|||
const val CAPTURE_AUDIO_INTENT = 46
|
||||
const val PICK_DOCUMENT_INTENT = 47
|
||||
const val PICK_CONTACT_INTENT = 48
|
||||
const val PICK_SAVE_DIR_INTENT = 50
|
||||
|
||||
const val BLOCKED_KEYWORDS_EXPORT_DELIMITER = ","
|
||||
const val BLOCKED_KEYWORDS_EXPORT_EXTENSION = ".txt"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package org.fossify.messages.models
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
|
|
@ -17,5 +17,5 @@ data class Attachment(
|
|||
@ColumnInfo(name = "filename") var filename: String
|
||||
) {
|
||||
|
||||
fun getUri() = Uri.parse(uriString)
|
||||
fun getUri() = uriString.toUri()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue