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 @@ + + + +