diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a40d3d85..ea66d25e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -195,6 +195,16 @@
+
+
+
+
()
private var messages = ArrayList()
private val availableSIMCards = ArrayList()
- private var attachmentUris = LinkedHashSet()
+ private var attachmentSelections = mutableMapOf()
+ private val imageCompressor by lazy { ImageCompressor(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -569,7 +572,7 @@ class ThreadActivity : SimpleActivity() {
conversationsDB.markRead(threadId)
}
- if (i == cnt - 1 && (message.type == Telephony.Sms.MESSAGE_TYPE_SENT )) {
+ if (i == cnt - 1 && (message.type == Telephony.Sms.MESSAGE_TYPE_SENT)) {
items.add(ThreadSent(message.id, delivered = message.status == Telephony.Sms.STATUS_COMPLETE))
}
}
@@ -592,23 +595,67 @@ class ThreadActivity : SimpleActivity() {
}
private fun addAttachment(uri: Uri) {
- if (attachmentUris.contains(uri)) {
+ val originalUriString = uri.toString()
+ if (attachmentSelections.containsKey(originalUriString)) {
return
}
- attachmentUris.add(uri)
+ attachmentSelections[originalUriString] = AttachmentSelection(uri, false)
+ val attachmentView = addAttachmentView(originalUriString, uri)
+ val mimeType = contentResolver.getType(uri)
+ Log.e(TAG, "Selected image: mimetype=$mimeType uri=$uri")
+ if (mimeType == null) {
+ Log.e(TAG, "addAttachment: null mime type for uri: $uri")
+ return
+ }
+
+ if (mimeType.isImageMimeType()) {
+ Log.d(TAG, "addAttachment: attachment is an image mimetype=$mimeType")
+ val byteArray = contentResolver.openInputStream(uri)?.readBytes()
+ if (byteArray == null) {
+ Log.e(TAG, "addAttachment: null stream for: $uri")
+ return
+ }
+
+ val selection = attachmentSelections[originalUriString]
+ attachmentSelections[originalUriString] = selection!!.copy(isPending = true)
+ checkSendMessageAvailability()
+ attachmentView.thread_attachment_progress.beVisible()
+ imageCompressor.compressImage(byteArray, mimeType, IMAGE_COMPRESS_SIZE) { compressedUri ->
+ runOnUiThread {
+ if (compressedUri != null) {
+ Log.e(TAG, "Compressed successfully compressedUri=$compressedUri")
+ attachmentSelections[originalUriString] = AttachmentSelection(compressedUri, false)
+ loadAttachmentPreview(attachmentView, compressedUri)
+ } else {
+ Log.e(TAG, "addAttachment: Failed to compress image: uri=$uri")
+ }
+ checkSendMessageAvailability()
+ attachmentView.thread_attachment_progress.beGone()
+ }
+ }
+ } else {
+ Log.d(TAG, "addAttachment: not an image")
+ }
+ }
+
+ private fun addAttachmentView(originalUri: String, uri: Uri): View {
thread_attachments_holder.beVisible()
val attachmentView = layoutInflater.inflate(R.layout.item_attachment, null).apply {
thread_attachments_wrapper.addView(this)
thread_remove_attachment.setOnClickListener {
thread_attachments_wrapper.removeView(this)
- attachmentUris.remove(uri)
- if (attachmentUris.isEmpty()) {
+ attachmentSelections.remove(originalUri)
+ if (attachmentSelections.isEmpty()) {
thread_attachments_holder.beGone()
}
}
}
+ loadAttachmentPreview(attachmentView, uri)
+ return attachmentView
+ }
+ private fun loadAttachmentPreview(attachmentView: View, uri: Uri) {
val roundedCornersRadius = resources.getDimension(R.dimen.medium_margin).toInt()
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
@@ -636,7 +683,7 @@ class ThreadActivity : SimpleActivity() {
}
private fun checkSendMessageAvailability() {
- if (thread_type_message.text.isNotEmpty() || attachmentUris.isNotEmpty()) {
+ if (thread_type_message.text.isNotEmpty() || (attachmentSelections.isNotEmpty() && !attachmentSelections.values.any { it.isPending })) {
thread_send_message.isClickable = true
thread_send_message.alpha = 0.9f
} else {
@@ -647,7 +694,7 @@ class ThreadActivity : SimpleActivity() {
private fun sendMessage() {
val msg = thread_type_message.value
- if (msg.isEmpty() && attachmentUris.isEmpty()) {
+ if (msg.isEmpty() && attachmentSelections.isEmpty()) {
return
}
@@ -673,15 +720,19 @@ class ThreadActivity : SimpleActivity() {
val transaction = Transaction(this, settings)
val message = com.klinker.android.send_message.Message(msg, numbers.toTypedArray())
- if (attachmentUris.isNotEmpty()) {
- for (uri in attachmentUris) {
+ if (attachmentSelections.isNotEmpty()) {
+ for (selection in attachmentSelections.values) {
+ Log.d(TAG, "sendMessage:attachmentUri=$selection")
try {
- val byteArray = contentResolver.openInputStream(uri)?.readBytes() ?: continue
- val mimeType = contentResolver.getType(uri) ?: continue
+ val byteArray = contentResolver.openInputStream(selection.uri)?.readBytes() ?: continue
+ val mimeType = contentResolver.getType(selection.uri) ?: continue
message.addMedia(byteArray, mimeType)
+ Log.d(TAG, "sendMessage: byteArray: ${byteArray.size} -- mimeType=$mimeType")
} catch (e: Exception) {
+ Log.e(TAG, "sendMessage: ", e)
showErrorToast(e)
} catch (e: Error) {
+ Log.e(TAG, "sendMessage error: ", e)
toast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
}
}
@@ -697,7 +748,7 @@ class ThreadActivity : SimpleActivity() {
refreshedSinceSent = false
transaction.sendNewMessage(message, threadId)
thread_type_message.setText("")
- attachmentUris.clear()
+ attachmentSelections.clear()
thread_attachments_holder.beGone()
thread_attachments_wrapper.removeAllViews()
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Bitmap.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Bitmap.kt
new file mode 100644
index 00000000..c55bbe7b
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Bitmap.kt
@@ -0,0 +1,9 @@
+package com.simplemobiletools.smsmessenger.extensions
+
+import android.graphics.Bitmap
+
+fun Bitmap.CompressFormat.extension() = when (this) {
+ Bitmap.CompressFormat.PNG -> "png"
+ Bitmap.CompressFormat.WEBP -> "webp"
+ else -> "jpg"
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt
new file mode 100644
index 00000000..5ad98f55
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt
@@ -0,0 +1,16 @@
+package com.simplemobiletools.smsmessenger.extensions
+
+fun String.getExtensionFromMimeType(): String {
+ return when (this) {
+ "image/png" -> ".png"
+ "image/apng" -> ".apng"
+ "image/webp" -> ".webp"
+ "image/svg+xml" -> ".svg"
+ "image/gif" -> ".gif"
+ else -> ".jpg"
+ }
+}
+
+fun String.isImageMimeType(): Boolean {
+ return startsWith("image")
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
index 36025476..820f8452 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
@@ -33,6 +33,8 @@ const val LOCK_SCREEN_SENDER_MESSAGE = 1
const val LOCK_SCREEN_SENDER = 2
const val LOCK_SCREEN_NOTHING = 3
+const val IMAGE_COMPRESS_SIZE = 1_048_576L
+
fun refreshMessages() {
EventBus.getDefault().post(Events.RefreshMessages())
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ImageCompressor.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ImageCompressor.kt
new file mode 100644
index 00000000..f642b2a4
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ImageCompressor.kt
@@ -0,0 +1,115 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import android.media.ExifInterface
+import android.net.Uri
+import android.util.Log
+import com.simplemobiletools.commons.extensions.getCompressionFormat
+import com.simplemobiletools.commons.extensions.getMyFileUri
+import com.simplemobiletools.commons.helpers.ensureBackgroundThread
+import com.simplemobiletools.smsmessenger.extensions.extension
+import com.simplemobiletools.smsmessenger.extensions.getExtensionFromMimeType
+import java.io.File
+import java.io.FileOutputStream
+
+/**
+ * Compress image to a given size based on
+ * [Compressor](https://github.com/zetbaitsu/Compressor/)
+ * */
+class ImageCompressor(private val context: Context) {
+ companion object {
+ private const val TAG = "ImageCompressor"
+ }
+
+ private val outputDirectory = File(context.cacheDir, "compressed").apply {
+ mkdirs()
+ }
+
+ fun compressImage(byteArray: ByteArray, mimeType: String, compressSize: Long, callback: (compressedFileUri: Uri?) -> Unit) {
+ ensureBackgroundThread {
+ try {
+ Log.d(TAG, "Attempting to compress image of length: ${byteArray.size} of mimetype=$mimeType to size=$compressSize")
+ var destinationFile = File(outputDirectory, System.currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
+ Log.d(TAG, "compressImage: Saving file to: $destinationFile")
+ destinationFile.writeBytes(byteArray)
+ Log.d(TAG, "Written file to: $destinationFile")
+ val constraint = SizeConstraint(compressSize)
+ Log.d(TAG, "Starting compression...")
+ while (constraint.isSatisfied(destinationFile).not()) {
+ destinationFile = constraint.satisfy(destinationFile)
+ Log.d(TAG, "Compressed, new size is ${destinationFile.length()}")
+ }
+
+ Log.d(TAG, "Compression done, new size is ${destinationFile.length()}")
+ callback.invoke(context.getMyFileUri(destinationFile))
+ } catch (e: Exception) {
+ Log.e(TAG, "compressImage: ", e)
+ callback.invoke(null)
+ }
+ }
+ }
+
+ private fun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.path.getCompressionFormat(), quality: Int = 100): File {
+ val result = if (format == imageFile.path.getCompressionFormat()) {
+ imageFile
+ } else {
+ File("${imageFile.absolutePath.substringBeforeLast(".")}.${format.extension()}")
+ }
+ imageFile.delete()
+ saveBitmap(bitmap, result, format, quality)
+ return result
+ }
+
+ private fun saveBitmap(bitmap: Bitmap, destination: File, format: Bitmap.CompressFormat = destination.path.getCompressionFormat(), quality: Int = 100) {
+ destination.parentFile?.mkdirs()
+ var fileOutputStream: FileOutputStream? = null
+ try {
+ fileOutputStream = FileOutputStream(destination.absolutePath)
+ bitmap.compress(format, quality, fileOutputStream)
+ } finally {
+ fileOutputStream?.run {
+ flush()
+ close()
+ }
+ }
+ }
+
+ private fun loadBitmap(imageFile: File) = BitmapFactory.decodeFile(imageFile.absolutePath).run {
+ determineImageRotation(imageFile, this)
+ }
+
+ private fun determineImageRotation(imageFile: File, bitmap: Bitmap): Bitmap {
+ val exif = ExifInterface(imageFile.absolutePath)
+ val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
+ val matrix = Matrix()
+ when (orientation) {
+ 6 -> matrix.postRotate(90f)
+ 3 -> matrix.postRotate(180f)
+ 8 -> matrix.postRotate(270f)
+ }
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ }
+
+ private inner class SizeConstraint(
+ private val maxFileSize: Long,
+ private val stepSize: Int = 10,
+ private val maxIteration: Int = 10,
+ private val minQuality: Int = 10
+ ) {
+ private var iteration: Int = 0
+
+ fun isSatisfied(imageFile: File): Boolean {
+ return imageFile.length() <= maxFileSize || iteration >= maxIteration
+ }
+
+ fun satisfy(imageFile: File): File {
+ iteration++
+ val quality = (100 - iteration * stepSize).takeIf { it >= minQuality } ?: minQuality
+ return overWrite(imageFile, loadBitmap(imageFile), quality = quality)
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt
new file mode 100644
index 00000000..e56835d2
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt
@@ -0,0 +1,8 @@
+package com.simplemobiletools.smsmessenger.models
+
+import android.net.Uri
+
+data class AttachmentSelection(
+ val uri: Uri,
+ val isPending: Boolean,
+)
diff --git a/app/src/main/res/layout/item_attachment.xml b/app/src/main/res/layout/item_attachment.xml
index ee25d173..8a620499 100644
--- a/app/src/main/res/layout/item_attachment.xml
+++ b/app/src/main/res/layout/item_attachment.xml
@@ -1,5 +1,6 @@
@@ -8,7 +9,16 @@
android:id="@+id/thread_attachment_preview"
android:layout_width="@dimen/attachment_preview_size"
android:layout_height="@dimen/attachment_preview_size"
- android:visibility="gone" />
+ android:visibility="gone"
+ tools:visibility="visible" />
+
+
+ android:visibility="gone"
+ tools:visibility="visible" />
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 00000000..d2c19fcd
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+