diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b669ba54..b12c0b35 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -93,17 +93,14 @@
-
-
-
+
-
-
+
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
index 9d7dde11..2f0263ec 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
@@ -5,7 +5,6 @@ import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.BitmapFactory
-import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.media.MediaMetadataRetriever
import android.net.Uri
@@ -27,21 +26,19 @@ import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.WindowManager
+import android.view.animation.OvershootInterpolator
import android.view.inputmethod.EditorInfo
import android.widget.LinearLayout
import android.widget.LinearLayout.LayoutParams
import android.widget.RelativeLayout
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.AppCompatButton
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.ResourcesCompat
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.DataSource
-import com.bumptech.glide.load.engine.DiskCacheStrategy
-import com.bumptech.glide.load.engine.GlideException
-import com.bumptech.glide.load.resource.bitmap.CenterCrop
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
-import com.bumptech.glide.request.RequestListener
-import com.bumptech.glide.request.RequestOptions
-import com.bumptech.glide.request.target.Target
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.children
+import androidx.core.view.updateLayoutParams
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
@@ -53,14 +50,17 @@ import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.commons.models.SimpleContact
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
+import com.simplemobiletools.smsmessenger.adapters.AttachmentsAdapter
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
+import ezvcard.VCard
+import ezvcard.property.FormattedName
+import ezvcard.property.Telephone
import kotlinx.android.synthetic.main.activity_thread.*
-import kotlinx.android.synthetic.main.item_attachment.view.*
import kotlinx.android.synthetic.main.item_selected_contact.view.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -72,12 +72,6 @@ import java.io.OutputStream
class ThreadActivity : SimpleActivity() {
private val MIN_DATE_TIME_DIFF_SECS = 300
- private val PICK_ATTACHMENT_INTENT = 1
- private val PICK_SAVE_FILE_INTENT = 11
- private val TAKE_PHOTO_INTENT = 42
-
- private val TYPE_TAKE_PHOTO = 12
- private val TYPE_CHOOSE_PHOTO = 13
private val TYPE_EDIT = 14
private val TYPE_SEND = 15
@@ -93,8 +87,6 @@ class ThreadActivity : SimpleActivity() {
private var privateContacts = ArrayList()
private var messages = ArrayList()
private val availableSIMCards = ArrayList()
- private var attachmentSelections = mutableMapOf()
- private val imageCompressor by lazy { ImageCompressor(this) }
private var lastAttachmentUri: String? = null
private var capturedImageUri: Uri? = null
private var loadingOlderMessages = false
@@ -105,6 +97,8 @@ class ThreadActivity : SimpleActivity() {
private var scheduledMessage: Message? = null
private lateinit var scheduledDateTime: DateTime
+ private var isAttachmentPickerVisible = false
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread)
@@ -145,6 +139,10 @@ class ThreadActivity : SimpleActivity() {
finish()
}
}
+
+ setupAttachmentPickerView()
+ setupKeyboardListener()
+ hideAttachmentPicker()
}
override fun onResume() {
@@ -161,7 +159,7 @@ class ThreadActivity : SimpleActivity() {
override fun onPause() {
super.onPause()
- if (thread_type_message.value != "" && attachmentSelections.isEmpty()) {
+ if (thread_type_message.value != "" && getAttachments().isEmpty()) {
saveSmsDraft(thread_type_message.value, threadId)
} else {
deleteSmsDraft(threadId)
@@ -172,6 +170,15 @@ class ThreadActivity : SimpleActivity() {
isActivityVisible = false
}
+ override fun onBackPressed() {
+ isAttachmentPickerVisible = false
+ if (attachment_picker_holder.isVisible()) {
+ hideAttachmentPicker()
+ } else {
+ super.onBackPressed()
+ }
+ }
+
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
@@ -214,13 +221,28 @@ class ThreadActivity : SimpleActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
super.onActivityResult(requestCode, resultCode, resultData)
if (resultCode != Activity.RESULT_OK) return
+ val data = resultData?.data
- if (requestCode == TAKE_PHOTO_INTENT) {
- addAttachment(capturedImageUri!!)
- } else if (requestCode == PICK_ATTACHMENT_INTENT && resultData != null && resultData.data != null) {
- addAttachment(resultData.data!!)
- } else if (requestCode == PICK_SAVE_FILE_INTENT && resultData != null && resultData.data != null) {
- saveAttachment(resultData)
+ when (requestCode) {
+ CAPTURE_PHOTO_INTENT -> addAttachment(capturedImageUri!!)
+ CAPTURE_VIDEO_INTENT -> if (data != null) {
+ addAttachment(data)
+ }
+ PICK_DOCUMENT_INTENT -> if (data != null) {
+ addAttachment(data)
+ }
+ CAPTURE_AUDIO_INTENT -> if (data != null) {
+ addAttachment(data)
+ }
+ PICK_PHOTO_VIDEO_INTENT -> if (data != null) {
+ addAttachment(data)
+ }
+ PICK_CONTACT_INTENT -> if (data != null) {
+ handleContactAttachment(data)
+ }
+ PICK_SAVE_FILE_INTENT -> if (data != null) {
+ saveAttachment(resultData)
+ }
}
}
@@ -487,7 +509,14 @@ class ThreadActivity : SimpleActivity() {
thread_type_message.setText(intent.getStringExtra(THREAD_TEXT))
thread_add_attachment.setOnClickListener {
- takeOrPickPhotoVideo()
+ if (attachment_picker_holder.isVisible()) {
+ isAttachmentPickerVisible = false
+ showKeyboard(thread_type_message)
+ } else {
+ isAttachmentPickerVisible = true
+ hideKeyboard()
+ }
+ window.decorView.requestApplyInsets()
}
if (intent.extras?.containsKey(THREAD_ATTACHMENT_URI) == true) {
@@ -780,135 +809,134 @@ class ThreadActivity : SimpleActivity() {
return items
}
- private fun takeOrPickPhotoVideo() {
- val items = arrayListOf(
- RadioItem(TYPE_TAKE_PHOTO, getString(R.string.take_photo)),
- RadioItem(TYPE_CHOOSE_PHOTO, getString(R.string.choose_photo))
- )
- RadioGroupDialog(this, items = items) {
- val checkedId = it as Int
- if (checkedId == TYPE_TAKE_PHOTO) {
- launchTakePhotoIntent()
- } else if (checkedId == TYPE_CHOOSE_PHOTO) {
- launchPickPhotoVideoIntent()
- }
- }
- }
-
- private fun launchTakePhotoIntent() {
- val imageFile = createImageFile()
- capturedImageUri = getMyFileUri(imageFile)
+ private fun launchActivityForResult(intent: Intent, requestCode: Int, @StringRes error: Int = R.string.no_app_found) {
+ hideKeyboard()
try {
- val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
- putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri)
- }
- startActivityForResult(intent, TAKE_PHOTO_INTENT)
+ startActivityForResult(intent, requestCode)
} catch (e: ActivityNotFoundException) {
- showErrorToast(getString(R.string.no_app_found))
+ showErrorToast(getString(error))
} catch (e: Exception) {
showErrorToast(e)
}
}
+ private fun createTemporaryFile(extension: String = ".jpg"): File {
+ val outputDirectory = File(cacheDir, "captured").apply {
+ if (!exists()) {
+ mkdirs()
+ }
+ }
+ return File.createTempFile("attachment_", extension, outputDirectory)
+ }
+
+ private fun launchCapturePhotoIntent() {
+ val imageFile = createTemporaryFile()
+ capturedImageUri = getMyFileUri(imageFile)
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
+ putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri)
+ }
+ launchActivityForResult(intent, CAPTURE_PHOTO_INTENT)
+ }
+
+ private fun launchCaptureVideoIntent() {
+ val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
+ launchActivityForResult(intent, CAPTURE_VIDEO_INTENT)
+ }
+
+ private fun launchCaptureAudioIntent() {
+ val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
+ launchActivityForResult(intent, CAPTURE_AUDIO_INTENT)
+ }
+
private fun launchPickPhotoVideoIntent() {
- hideKeyboard()
val mimeTypes = arrayOf("image/*", "video/*")
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
- try {
- startActivityForResult(this, PICK_ATTACHMENT_INTENT)
- } catch (e: ActivityNotFoundException) {
- showErrorToast(getString(R.string.no_app_found))
- } catch (e: Exception) {
- showErrorToast(e)
+ launchActivityForResult(this, PICK_PHOTO_VIDEO_INTENT)
+ }
+ }
+
+ private fun launchPickDocumentIntent() {
+ val mimeTypes = arrayOf("*/*")
+ Intent(Intent.ACTION_GET_CONTENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "*/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
+
+ launchActivityForResult(this, PICK_DOCUMENT_INTENT)
+ }
+ }
+
+ private fun launchPickContactIntent() {
+ val intent = Intent(Intent.ACTION_PICK).apply {
+ type = ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE
+ }
+ launchActivityForResult(intent, PICK_CONTACT_INTENT)
+ }
+
+ private fun handleContactAttachment(contactUri: Uri) {
+ val projection = arrayOf(
+ ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
+ ContactsContract.CommonDataKinds.Phone.NUMBER
+ )
+ queryCursor(contactUri, projection, null, null, null) { cursor ->
+ if (cursor.moveToFirst()) {
+ val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
+ val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
+ val name = cursor.getString(nameIndex)
+ val number = cursor.getString(numberIndex)
+ // todo: export all properties using VcfExporter
+ val vCard = VCard()
+ vCard.addTelephoneNumber(Telephone(number))
+ vCard.addFormattedName(FormattedName(name))
+ val file = createTemporaryFile(".vcf")
+ val outputStream = file.outputStream()
+ vCard.write(outputStream)
+ val vCardUri = getMyFileUri(file)
+ addAttachment(vCardUri)
}
}
}
+ private fun getAttachmentsAdapter(): AttachmentsAdapter? {
+ val adapter = thread_attachments_recyclerview.adapter
+ return adapter as? AttachmentsAdapter
+ }
+
+ private fun getAttachments() = getAttachmentsAdapter()?.attachments ?: emptyList()
+
private fun addAttachment(uri: Uri) {
- val originalUriString = uri.toString()
- if (attachmentSelections.containsKey(originalUriString)) {
+ if (getAttachments().any { it.uri.toString() == uri.toString() }) {
return
}
- attachmentSelections[originalUriString] = AttachmentSelection(uri, false)
- val attachmentView = addAttachmentView(originalUriString, uri)
- val mimeType = contentResolver.getType(uri) ?: return
-
- if (mimeType.isImageMimeType() && config.mmsFileSizeLimit != FILE_SIZE_NONE) {
- val selection = attachmentSelections[originalUriString]
- attachmentSelections[originalUriString] = selection!!.copy(isPending = true)
- checkSendMessageAvailability()
- attachmentView.thread_attachment_progress.beVisible()
- imageCompressor.compressImage(uri, config.mmsFileSizeLimit) { compressedUri ->
- runOnUiThread {
- if (compressedUri != null) {
- attachmentSelections[originalUriString] = AttachmentSelection(compressedUri, false)
- loadAttachmentPreview(attachmentView, compressedUri)
- } else {
- toast(R.string.compress_error)
- removeAttachment(attachmentView, originalUriString)
- }
+ var adapter = getAttachmentsAdapter()
+ if (adapter == null) {
+ adapter = AttachmentsAdapter(
+ activity = this,
+ onItemClick = {},
+ onAttachmentsRemoved = {
+ thread_attachments_recyclerview.beGone()
checkSendMessageAvailability()
- attachmentView.thread_attachment_progress.beGone()
- }
- }
- }
- }
-
- 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 {
- removeAttachment(this, originalUri)
- }
+ },
+ onReady = { checkSendMessageAvailability() }
+ )
+ thread_attachments_recyclerview.adapter = adapter
}
- loadAttachmentPreview(attachmentView, uri)
- return attachmentView
- }
+ thread_attachments_recyclerview.beVisible()
+ val mimeType = contentResolver.getType(uri).orEmpty()
+ val attachment = AttachmentSelection(
+ uri = uri,
+ mimetype = mimeType,
+ filename = getFilenameFromUri(uri),
+ isPending = mimeType.isImageMimeType()
+ )
+ adapter.addAttachment(attachment)
- private fun loadAttachmentPreview(attachmentView: View, uri: Uri) {
- if (isDestroyed || isFinishing) {
- return
- }
-
- val roundedCornersRadius = resources.getDimension(R.dimen.medium_margin).toInt()
- val options = RequestOptions()
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .transform(CenterCrop(), RoundedCorners(roundedCornersRadius))
-
- Glide.with(attachmentView.thread_attachment_preview)
- .load(uri)
- .transition(DrawableTransitionOptions.withCrossFade())
- .apply(options)
- .listener(object : RequestListener {
- override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean {
- attachmentView.thread_attachment_preview.beGone()
- attachmentView.thread_remove_attachment.beGone()
- return false
- }
-
- override fun onResourceReady(dr: Drawable?, a: Any?, t: Target?, d: DataSource?, i: Boolean): Boolean {
- attachmentView.thread_attachment_preview.beVisible()
- attachmentView.thread_remove_attachment.beVisible()
- checkSendMessageAvailability()
- return false
- }
- })
- .into(attachmentView.thread_attachment_preview)
- }
-
- private fun removeAttachment(attachmentView: View, originalUri: String) {
- thread_attachments_wrapper.removeView(attachmentView)
- attachmentSelections.remove(originalUri)
- if (attachmentSelections.isEmpty()) {
- thread_attachments_holder.beGone()
- }
checkSendMessageAvailability()
}
@@ -933,7 +961,7 @@ class ThreadActivity : SimpleActivity() {
}
private fun checkSendMessageAvailability() {
- if (thread_type_message.text!!.isNotEmpty() || (attachmentSelections.isNotEmpty() && !attachmentSelections.values.any { it.isPending })) {
+ if (thread_type_message.text!!.isNotEmpty() || (getAttachments().isNotEmpty() && !getAttachments().any { it.isPending })) {
thread_send_message.isEnabled = true
thread_send_message.isClickable = true
thread_send_message.alpha = 0.9f
@@ -947,7 +975,7 @@ class ThreadActivity : SimpleActivity() {
private fun sendMessage() {
var text = thread_type_message.value
- if (text.isEmpty() && attachmentSelections.isEmpty()) {
+ if (text.isEmpty() && getAttachments().isEmpty()) {
showErrorToast(getString(R.string.unknown_error_occurred))
return
}
@@ -1002,8 +1030,7 @@ class ThreadActivity : SimpleActivity() {
private fun sendNormalMessage(text: String, subscriptionId: Int) {
val addresses = participants.getAddresses()
- val attachments = attachmentSelections.values
- .map { it.uri }
+ val attachments = getAttachments().map { it.uri }
try {
refreshedSinceSent = false
@@ -1022,9 +1049,8 @@ class ThreadActivity : SimpleActivity() {
private fun clearCurrentMessage() {
thread_type_message.setText("")
- attachmentSelections.clear()
- thread_attachments_holder.beGone()
- thread_attachments_wrapper.removeAllViews()
+ getAttachmentsAdapter()?.clear()
+ checkSendMessageAvailability()
}
// show selected contacts, properly split to new lines when appropriate
@@ -1150,13 +1176,7 @@ class ThreadActivity : SimpleActivity() {
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_TITLE, path.split("/").last())
- try {
- startActivityForResult(this, PICK_SAVE_FILE_INTENT)
- } catch (e: ActivityNotFoundException) {
- showErrorToast(getString(R.string.system_service_disabled))
- } catch (e: Exception) {
- showErrorToast(e)
- }
+ launchActivityForResult(this, PICK_SAVE_FILE_INTENT, error = R.string.system_service_disabled)
}
}
@@ -1204,7 +1224,7 @@ class ThreadActivity : SimpleActivity() {
private fun isMmsMessage(text: String): Boolean {
val isGroupMms = participants.size > 1 && config.sendGroupMessageMMS
val isLongMmsMessage = isLongMmsMessage(text) && config.sendLongMessageMMS
- return attachmentSelections.isNotEmpty() || isGroupMms || isLongMmsMessage
+ return getAttachments().isNotEmpty() || isGroupMms || isLongMmsMessage
}
private fun updateMessageType() {
@@ -1217,15 +1237,6 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.setText(stringId)
}
- private fun createImageFile(): File {
- val outputDirectory = File(cacheDir, "captured").apply {
- if (!exists()) {
- mkdirs()
- }
- }
- return File.createTempFile("IMG_", ".jpg", outputDirectory)
- }
-
private fun showScheduledMessageInfo(message: Message) {
val items = arrayListOf(
RadioItem(TYPE_EDIT, getString(R.string.update_message)),
@@ -1282,7 +1293,7 @@ class ThreadActivity : SimpleActivity() {
private fun setupScheduleSendUi() {
val textColor = getProperTextColor()
- scheduled_message_holder.background.applyColorFilter(getProperBackgroundColor().getContrastColor())
+ scheduled_message_holder.background.applyColorFilter(getProperPrimaryColor().darkenColor())
scheduled_message_button.apply {
val clockDrawable = ResourcesCompat.getDrawable(resources, R.drawable.ic_clock_vector, theme)?.apply { applyColorFilter(textColor) }
setCompoundDrawablesWithIntrinsicBounds(clockDrawable, null, null, null)
@@ -1358,10 +1369,94 @@ class ThreadActivity : SimpleActivity() {
}
private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment {
- val attachments = attachmentSelections.values
+ val attachments = getAttachments()
.map { Attachment(null, messageId, it.uri.toString(), contentResolver.getType(it.uri) ?: "*/*", 0, 0, "") }
.toArrayList()
return MessageAttachment(messageId, text, attachments)
}
+
+ private fun setupAttachmentPickerView() {
+ val colors = arrayOf(
+ R.color.md_red_500,
+ R.color.md_pink_500,
+ R.color.md_purple_500,
+ R.color.md_teal_500,
+ R.color.md_green_500,
+ R.color.md_light_green_500,
+ R.color.md_blue_500
+ )
+ attachment_picker_holder.children.filterIsInstance().forEachIndexed { index, button ->
+ button.setTextColor(getProperTextColor())
+ val color = resources.getColor(colors[index])
+ button.compoundDrawables.forEach { it?.applyColorFilter(color) }
+ }
+ pick_from_gallery.setOnClickListener {
+ launchPickPhotoVideoIntent()
+ }
+ camera.setOnClickListener {
+ launchCapturePhotoIntent()
+ }
+ record_video.setOnClickListener {
+ launchCaptureVideoIntent()
+ }
+ record_audio.setOnClickListener {
+ launchCaptureAudioIntent()
+ }
+ pick_file.setOnClickListener {
+ launchPickDocumentIntent()
+ }
+ pick_contact.setOnClickListener {
+ launchPickContactIntent()
+ }
+ schedule_message.setOnClickListener {
+ if (isScheduledMessage) {
+ launchScheduleSendDialog(scheduledDateTime)
+ } else {
+ launchScheduleSendDialog()
+ }
+ }
+ }
+
+ private fun showAttachmentPicker() {
+ attachment_picker_divider.showWithAnimation()
+ attachment_picker_holder.showWithAnimation()
+ animateAttachmentButton(rotation = -135f)
+ }
+
+ private fun hideAttachmentPicker() {
+ attachment_picker_divider.beGone()
+ attachment_picker_holder.apply {
+ beGone()
+ updateLayoutParams {
+ height = config.keyboardHeight
+ }
+ }
+ animateAttachmentButton(rotation = 0f)
+ }
+
+ private fun animateAttachmentButton(rotation: Float) {
+ thread_add_attachment.animate()
+ .rotation(rotation)
+ .setDuration(500L)
+ .setInterpolator(OvershootInterpolator())
+ .start()
+ }
+
+ private fun setupKeyboardListener() {
+ val typeMask = WindowInsetsCompat.Type.ime()
+
+ ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { _, insets ->
+ if (insets.isVisible(typeMask)) {
+ config.keyboardHeight = insets.getInsets(typeMask).bottom
+ hideAttachmentPicker()
+ } else {
+ if (isAttachmentPickerVisible) {
+ showAttachmentPicker()
+ }
+ }
+
+ insets
+ }
+ }
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt
new file mode 100644
index 00000000..37baa75b
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt
@@ -0,0 +1,227 @@
+package com.simplemobiletools.smsmessenger.adapters
+
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+import com.simplemobiletools.commons.activities.BaseSimpleActivity
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.smsmessenger.activities.VCardViewerActivity
+import com.simplemobiletools.smsmessenger.extensions.config
+import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
+import com.simplemobiletools.smsmessenger.extensions.isVideoMimeType
+import com.simplemobiletools.smsmessenger.extensions.launchViewIntent
+import com.simplemobiletools.smsmessenger.helpers.*
+import com.simplemobiletools.smsmessenger.models.AttachmentSelection
+import kotlinx.android.synthetic.main.item_attachment_media_preview.view.*
+import kotlinx.android.synthetic.main.item_remove_attachment_button.view.*
+
+class AttachmentsAdapter(
+ val activity: BaseSimpleActivity,
+ val onItemClick: (AttachmentSelection) -> Unit,
+ val onAttachmentsRemoved: () -> Unit,
+ val onReady: (() -> Unit)
+) : ListAdapter(AttachmentDiffCallback()) {
+
+ private val config = activity.config
+ private val resources = activity.resources
+ private val primaryColor = activity.getProperPrimaryColor()
+ private val imageCompressor by lazy { ImageCompressor(activity) }
+
+ val attachments = mutableListOf()
+
+ fun clear() {
+ attachments.clear()
+ submitList(ArrayList())
+ onAttachmentsRemoved()
+ }
+
+ fun addAttachment(attachment: AttachmentSelection) {
+ attachments.removeAll { AttachmentSelection.areItemsTheSame(it, attachment) }
+ attachments.add(attachment)
+ submitList(attachments.toList())
+ }
+
+ private fun removeAttachment(attachment: AttachmentSelection) {
+ attachments.removeAll { AttachmentSelection.areItemsTheSame(it, attachment) }
+ if (attachments.isEmpty()) {
+ onAttachmentsRemoved()
+ } else {
+ submitList(ArrayList(attachments))
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return getItem(position).viewType
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val layoutRes = when (viewType) {
+ ATTACHMENT_DOCUMENT -> com.simplemobiletools.smsmessenger.R.layout.item_attachment_document_preview
+ ATTACHMENT_VCARD -> com.simplemobiletools.smsmessenger.R.layout.item_attachment_vcard_preview
+ ATTACHMENT_MEDIA -> com.simplemobiletools.smsmessenger.R.layout.item_attachment_media_preview
+ else -> throw IllegalArgumentException("Unknown view type: $viewType")
+ }
+
+ val view = activity.layoutInflater.inflate(layoutRes, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val attachment = getItem(position)
+ holder.bindView(attachment, allowSingleClick = true, allowLongClick = false) { view, position ->
+ when (attachment.viewType) {
+ ATTACHMENT_DOCUMENT -> {
+ view.setupDocumentPreview(
+ uri = attachment.uri,
+ title = attachment.filename,
+ attachment = true,
+ onClick = { activity.launchViewIntent(attachment.uri, attachment.mimetype, attachment.filename) },
+ onRemoveButtonClicked = { removeAttachment(attachment) }
+ )
+ }
+ ATTACHMENT_VCARD -> {
+ view.setupVCardPreview(
+ activity = activity,
+ uri = attachment.uri,
+ attachment = true,
+ onClick = {
+ val intent = Intent(activity, VCardViewerActivity::class.java).also {
+ it.putExtra(EXTRA_VCARD_URI, attachment.uri)
+ }
+ activity.startActivity(intent)
+ },
+ onRemoveButtonClicked = { removeAttachment(attachment) }
+ )
+ }
+ ATTACHMENT_MEDIA -> setupMediaPreview(view, attachment)
+ }
+ }
+ }
+
+ private fun setupMediaPreview(view: View, attachment: AttachmentSelection) {
+ view.apply {
+ media_attachment_holder.background.applyColorFilter(primaryColor.darkenColor())
+ media_attachment_holder.setOnClickListener {
+ activity.launchViewIntent(attachment.uri, attachment.mimetype, attachment.filename)
+ }
+ remove_attachment_button.apply {
+ beVisible()
+ background.applyColorFilter(primaryColor)
+ setOnClickListener {
+ removeAttachment(attachment)
+ }
+ }
+
+ if (attachment.mimetype.isImageMimeType() && attachment.isPending && config.mmsFileSizeLimit != FILE_SIZE_NONE) {
+ thumbnail.beGone()
+ compression_progress.beVisible()
+
+ imageCompressor.compressImage(attachment.uri, config.mmsFileSizeLimit) { compressedUri ->
+ activity.runOnUiThread {
+ when (compressedUri) {
+ attachment.uri -> {
+ attachments.find { it.uri == attachment.uri }?.isPending = false
+ loadMediaPreview(view, attachment)
+ }
+ null -> {
+ activity.toast(com.simplemobiletools.smsmessenger.R.string.compress_error)
+ removeAttachment(attachment)
+ }
+ else -> {
+ attachments.remove(attachment)
+ addAttachment(attachment.copy(uri = compressedUri, isPending = false))
+ }
+ }
+ onReady()
+ }
+ }
+ } else {
+ loadMediaPreview(view, attachment)
+ }
+ }
+ }
+
+ private fun loadMediaPreview(view: View, attachment: AttachmentSelection) {
+ val roundedCornersRadius = resources.getDimension(com.simplemobiletools.smsmessenger.R.dimen.activity_margin).toInt()
+ val size = resources.getDimension(com.simplemobiletools.smsmessenger.R.dimen.attachment_preview_size).toInt()
+
+ val options = RequestOptions()
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .transform(CenterCrop(), RoundedCorners(roundedCornersRadius))
+
+ Glide.with(view.thumbnail)
+ .load(attachment.uri)
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .override(size, size)
+ .apply(options)
+ .listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean {
+ removeAttachment(attachment)
+ activity.toast(com.simplemobiletools.smsmessenger.R.string.unknown_error_occurred)
+ return false
+ }
+
+ override fun onResourceReady(dr: Drawable?, a: Any?, t: Target?, d: DataSource?, i: Boolean): Boolean {
+ view.thumbnail.beVisible()
+ view.play_icon.beVisibleIf(attachment.mimetype.isVideoMimeType())
+ view.compression_progress.beGone()
+ return false
+ }
+ })
+ .into(view.thumbnail)
+ }
+
+ open inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ fun bindView(
+ any: AttachmentSelection,
+ allowSingleClick: Boolean,
+ allowLongClick: Boolean,
+ callback: (itemView: View, adapterPosition: Int) -> Unit
+ ): View {
+ return itemView.apply {
+ callback(this, adapterPosition)
+
+ if (allowSingleClick) {
+ setOnClickListener { viewClicked(any) }
+ setOnLongClickListener { if (allowLongClick) viewLongClicked() else viewClicked(any); true }
+ } else {
+ setOnClickListener(null)
+ setOnLongClickListener(null)
+ }
+ }
+ }
+
+ private fun viewClicked(any: AttachmentSelection) {
+ onItemClick.invoke(any)
+ }
+
+ private fun viewLongClicked() {
+
+ }
+ }
+}
+
+private class AttachmentDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: AttachmentSelection, newItem: AttachmentSelection): Boolean {
+ return AttachmentSelection.areItemsTheSame(oldItem, newItem)
+ }
+
+ override fun areContentsTheSame(oldItem: AttachmentSelection, newItem: AttachmentSelection): Boolean {
+ return AttachmentSelection.areContentsTheSame(oldItem, newItem)
+ }
+
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
index 06366948..9aa0f4ee 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt
@@ -1,13 +1,11 @@
package com.simplemobiletools.smsmessenger.adapters
import android.annotation.SuppressLint
-import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
-import android.net.Uri
import android.util.TypedValue
import android.view.Menu
import android.view.View
@@ -38,7 +36,6 @@ import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
import kotlinx.android.synthetic.main.item_attachment_image.view.*
-import kotlinx.android.synthetic.main.item_attachment_vcard.view.*
import kotlinx.android.synthetic.main.item_received_message.view.*
import kotlinx.android.synthetic.main.item_received_message.view.thread_mesage_attachments_holder
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_body
@@ -49,8 +46,6 @@ import kotlinx.android.synthetic.main.item_thread_date_time.view.*
import kotlinx.android.synthetic.main.item_thread_error.view.*
import kotlinx.android.synthetic.main.item_thread_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.*
-import kotlinx.android.synthetic.main.item_unknown_attachment.view.*
-import java.util.*
class ThreadAdapter(
activity: SimpleActivity, var messages: ArrayList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit
@@ -386,7 +381,7 @@ class ThreadAdapter(
if (actModeCallback.isSelectable) {
holder.viewClicked(message)
} else {
- launchViewIntent(uri, mimetype, attachment.filename)
+ activity.launchViewIntent(uri, mimetype, attachment.filename)
}
}
imageView.setOnLongClickListener {
@@ -400,50 +395,23 @@ class ThreadAdapter(
val uri = attachment.getUri()
parent.apply {
val vCardView = layoutInflater.inflate(R.layout.item_attachment_vcard, null).apply {
- background.applyColorFilter(backgroundColor.getContrastColor())
- vcard_title.setTextColor(textColor)
- vcard_subtitle.setTextColor(textColor)
- view_contact_details.setTextColor(properPrimaryColor)
+ setupVCardPreview(
+ activity = activity,
+ uri = uri,
+ onClick = {
+ if (actModeCallback.isSelectable) {
+ holder.viewClicked(message)
+ } else {
+ val intent = Intent(context, VCardViewerActivity::class.java).also {
+ it.putExtra(EXTRA_VCARD_URI, uri)
+ }
+ context.startActivity(intent)
+ }
+ },
+ onLongClick = { holder.viewLongClicked() }
+ )
}
thread_mesage_attachments_holder.addView(vCardView)
-
- parseVCardFromUri(context, uri) { vCards ->
- val title = vCards.firstOrNull()?.parseNameFromVCard()
- val imageIcon = if (title != null) {
- SimpleContactsHelper(context).getContactLetterIcon(title)
- } else {
- null
- }
- activity.runOnUiThread {
- vCardView.apply {
- vcard_title.text = title
- vcard_photo.setImageBitmap(imageIcon)
-
- if (vCards.size > 1) {
- vcard_subtitle.beVisible()
- val quantity = vCards.size - 1
- vcard_subtitle.text = resources.getQuantityString(R.plurals.and_other_contacts, quantity, quantity)
- } else {
- vcard_subtitle.beGone()
- }
-
- setOnClickListener {
- if (actModeCallback.isSelectable) {
- holder.viewClicked(message)
- } else {
- val intent = Intent(context, VCardViewerActivity::class.java).also {
- it.putExtra(EXTRA_VCARD_URI, uri)
- }
- context.startActivity(intent)
- }
- }
- setOnLongClickListener {
- holder.viewLongClicked()
- true
- }
- }
- }
- }
}
}
@@ -451,66 +419,24 @@ class ThreadAdapter(
val mimetype = attachment.mimetype
val uri = attachment.getUri()
parent.apply {
- val attachmentView = layoutInflater.inflate(R.layout.item_unknown_attachment, null).apply {
- if (attachment.filename.isNotEmpty()) {
- filename.text = attachment.filename
- }
-
- val size = context.contentResolver
- .openInputStream(uri)
- ?.use { it.readBytes() }
- ?.size
-
- if (size != null) {
- file_size.beVisible()
- file_size.text = size.formatSize()
- } else {
- file_size.beGone()
- }
-
- background.applyColorFilter(textColor)
- filename.setTextColor(textColor)
- file_size.setTextColor(textColor)
- icon.background.setTint(properPrimaryColor)
-
- setOnClickListener {
- if (actModeCallback.isSelectable) {
- holder.viewClicked(message)
- } else {
- launchViewIntent(uri, mimetype, attachment.filename)
- }
- }
- setOnLongClickListener {
- holder.viewLongClicked()
- true
- }
+ val attachmentView = layoutInflater.inflate(R.layout.item_attachment_document, null).apply {
+ setupDocumentPreview(
+ uri = uri,
+ title = attachment.filename,
+ onClick = {
+ if (actModeCallback.isSelectable) {
+ holder.viewClicked(message)
+ } else {
+ activity.launchViewIntent(uri, mimetype, attachment.filename)
+ }
+ },
+ onLongClick = { holder.viewLongClicked() },
+ )
}
thread_mesage_attachments_holder.addView(attachmentView)
}
}
- private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) {
- Intent().apply {
- action = Intent.ACTION_VIEW
- setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
-
- try {
- activity.hideKeyboard()
- activity.startActivity(this)
- } catch (e: ActivityNotFoundException) {
- val newMimetype = filename.getMimeType()
- if (newMimetype.isNotEmpty() && mimetype != newMimetype) {
- launchViewIntent(uri, newMimetype, filename)
- } else {
- activity.toast(R.string.no_app_found)
- }
- } catch (e: Exception) {
- activity.showErrorToast(e)
- }
- }
- }
-
private fun setupDateTime(view: View, dateTime: ThreadDateTime) {
view.apply {
thread_date_time.apply {
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Activity.kt
index 1d165091..8bbd408c 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Activity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Activity.kt
@@ -4,10 +4,12 @@ import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
+import com.simplemobiletools.commons.extensions.getMimeType
import com.simplemobiletools.commons.extensions.hideKeyboard
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.smsmessenger.R
+import java.util.*
fun Activity.dialNumber(phoneNumber: String, callback: (() -> Unit)? = null) {
hideKeyboard()
@@ -24,3 +26,25 @@ fun Activity.dialNumber(phoneNumber: String, callback: (() -> Unit)? = null) {
}
}
}
+
+fun Activity.launchViewIntent(uri: Uri, mimetype: String, filename: String) {
+ Intent().apply {
+ action = Intent.ACTION_VIEW
+ setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ try {
+ hideKeyboard()
+ startActivity(this)
+ } catch (e: ActivityNotFoundException) {
+ val newMimetype = filename.getMimeType()
+ if (newMimetype.isNotEmpty() && mimetype != newMimetype) {
+ launchViewIntent(uri, newMimetype, filename)
+ } else {
+ toast(R.string.no_app_found)
+ }
+ } catch (e: Exception) {
+ showErrorToast(e)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
index c285ec98..17cc4b90 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
@@ -335,6 +335,7 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, attachmentName)
messageAttachment.attachments.add(attachment)
} else {
+ // todo: fix filename parsing, xml is shown some sometimes
val text = cursor.getStringValue(Mms.Part.TEXT)
val cutName = text.substringAfter("ref src=\"").substringBefore("\"")
if (cutName.isNotEmpty()) {
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/View.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/View.kt
new file mode 100644
index 00000000..47ff0dba
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/View.kt
@@ -0,0 +1,18 @@
+package com.simplemobiletools.smsmessenger.extensions
+
+import android.animation.ObjectAnimator
+import android.view.View
+import androidx.core.animation.doOnStart
+import androidx.core.view.isVisible
+
+fun View.showWithAnimation(duration: Long = 250L) {
+ if (!isVisible) {
+ ObjectAnimator.ofFloat(
+ this, "alpha", 0f, 1f
+ ).apply {
+ this.duration = duration
+ doOnStart { visibility = View.VISIBLE }
+ }.start()
+ }
+}
+
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
index 57c5ad8d..bec52c26 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
@@ -86,4 +86,8 @@ class Config(context: Context) : BaseConfig(context) {
var wasDbCleared: Boolean
get() = prefs.getBoolean(WAS_DB_CLEARED, false)
set(wasDbCleared) = prefs.edit().putBoolean(WAS_DB_CLEARED, wasDbCleared).apply()
+
+ var keyboardHeight: Int
+ get() = prefs.getInt(SOFT_KEYBOARD_HEIGHT, 600)
+ set(value) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, value).apply()
}
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 15baf492..2e96f376 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
@@ -30,6 +30,7 @@ const val IMPORT_MMS = "import_mms"
const val WAS_DB_CLEARED = "was_db_cleared_2"
const val EXTRA_VCARD_URI = "vcard"
const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
+const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"
@@ -45,6 +46,11 @@ const val THREAD_SENT_MESSAGE_ERROR = 4
const val THREAD_SENT_MESSAGE_SENT = 5
const val THREAD_SENT_MESSAGE_SENDING = 6
+// view types for attachment list
+const val ATTACHMENT_DOCUMENT = 7
+const val ATTACHMENT_MEDIA = 8
+const val ATTACHMENT_VCARD = 9
+
// lock screen visibility constants
const val LOCK_SCREEN_SENDER_MESSAGE = 1
const val LOCK_SCREEN_SENDER = 2
@@ -60,6 +66,15 @@ const val FILE_SIZE_2_MB = 2_097_152L
const val MESSAGES_LIMIT = 50
+// intent launch request codes
+const val PICK_PHOTO_VIDEO_INTENT = 42
+const val PICK_SAVE_FILE_INTENT = 43
+const val CAPTURE_PHOTO_INTENT = 44
+const val CAPTURE_VIDEO_INTENT = 45
+const val CAPTURE_AUDIO_INTENT = 46
+const val PICK_DOCUMENT_INTENT = 47
+const val PICK_CONTACT_INTENT = 48
+
fun refreshMessages() {
EventBus.getDefault().post(Events.RefreshMessages())
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/DocumentPreview.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/DocumentPreview.kt
new file mode 100644
index 00000000..f737a823
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/DocumentPreview.kt
@@ -0,0 +1,55 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import android.net.Uri
+import android.view.View
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.smsmessenger.extensions.getFileSizeFromUri
+import kotlinx.android.synthetic.main.item_attachment_document.view.*
+import kotlinx.android.synthetic.main.item_remove_attachment_button.view.*
+
+fun View.setupDocumentPreview(
+ uri: Uri,
+ title: String,
+ attachment: Boolean = false,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null,
+ onRemoveButtonClicked: (() -> Unit)? = null
+) {
+ if (title.isNotEmpty()) {
+ filename.text = title
+ }
+
+ val size = context.getFileSizeFromUri(uri)
+ file_size.beVisible()
+ file_size.text = size.formatSize()
+
+ val textColor = context.getProperTextColor()
+ val primaryColor = context.getProperPrimaryColor()
+
+ document_attachment_holder.background.applyColorFilter(textColor)
+ filename.setTextColor(textColor)
+ file_size.setTextColor(textColor)
+ // todo: set icon drawable based on mime type
+ icon.background.setTint(primaryColor)
+ document_attachment_holder.background.applyColorFilter(primaryColor.darkenColor())
+
+ if (attachment) {
+ remove_attachment_button.apply {
+ beVisible()
+ background.applyColorFilter(primaryColor)
+ if (onRemoveButtonClicked != null) {
+ setOnClickListener {
+ onRemoveButtonClicked.invoke()
+ }
+ }
+ }
+ }
+
+ document_attachment_holder.setOnClickListener {
+ onClick?.invoke()
+ }
+ document_attachment_holder.setOnLongClickListener {
+ onLongClick?.invoke()
+ true
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardPreview.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardPreview.kt
new file mode 100644
index 00000000..8cf7052c
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardPreview.kt
@@ -0,0 +1,83 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import android.app.Activity
+import android.net.Uri
+import android.view.View
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.helpers.SimpleContactsHelper
+import com.simplemobiletools.smsmessenger.R
+import kotlinx.android.synthetic.main.item_attachment_vcard.view.*
+import kotlinx.android.synthetic.main.item_attachment_vcard_preview.view.*
+import kotlinx.android.synthetic.main.item_remove_attachment_button.view.*
+
+fun View.setupVCardPreview(
+ activity: Activity,
+ uri: Uri,
+ attachment: Boolean = false,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null,
+ onRemoveButtonClicked: (() -> Unit)? = null,
+) {
+ val textColor = activity.getProperTextColor()
+ val primaryColor = activity.getProperPrimaryColor()
+
+ vcard_attachment_holder.background.applyColorFilter(primaryColor.darkenColor())
+ vcard_title.setTextColor(textColor)
+ vcard_subtitle.setTextColor(textColor)
+
+ if (attachment) {
+ vcard_progress.beVisible()
+ }
+ arrayOf(vcard_photo, vcard_title, vcard_subtitle, view_contact_details).forEach {
+ it.beGone()
+ }
+
+ parseVCardFromUri(activity, uri) { vCards ->
+ val title = vCards.firstOrNull()?.parseNameFromVCard()
+ val imageIcon = if (title != null) {
+ SimpleContactsHelper(activity).getContactLetterIcon(title)
+ } else {
+ null
+ }
+ activity.runOnUiThread {
+ arrayOf(vcard_photo, vcard_title).forEach {
+ it.beVisible()
+ }
+
+ vcard_photo.setImageBitmap(imageIcon)
+ vcard_title.text = title
+
+ if (vCards.size > 1) {
+ vcard_subtitle.beVisible()
+ val quantity = vCards.size - 1
+ vcard_subtitle.text = resources.getQuantityString(R.plurals.and_other_contacts, quantity, quantity)
+ } else {
+ vcard_subtitle.beGone()
+ }
+
+ if (attachment) {
+ vcard_progress.beGone()
+ remove_attachment_button.apply {
+ beVisible()
+ background.applyColorFilter(primaryColor)
+ if (onRemoveButtonClicked != null) {
+ setOnClickListener {
+ onRemoveButtonClicked.invoke()
+ }
+ }
+ }
+ } else {
+ view_contact_details.setTextColor(primaryColor)
+ view_contact_details.beVisible()
+ }
+
+ vcard_attachment_holder.setOnClickListener {
+ onClick?.invoke()
+ }
+ vcard_attachment_holder.setOnLongClickListener {
+ onLongClick?.invoke()
+ true
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt
index e56835d2..eb81fda2 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/AttachmentSelection.kt
@@ -1,8 +1,35 @@
package com.simplemobiletools.smsmessenger.models
import android.net.Uri
+import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
+import com.simplemobiletools.smsmessenger.extensions.isVCardMimeType
+import com.simplemobiletools.smsmessenger.extensions.isVideoMimeType
+import com.simplemobiletools.smsmessenger.helpers.ATTACHMENT_DOCUMENT
+import com.simplemobiletools.smsmessenger.helpers.ATTACHMENT_MEDIA
+import com.simplemobiletools.smsmessenger.helpers.ATTACHMENT_VCARD
data class AttachmentSelection(
val uri: Uri,
- val isPending: Boolean,
-)
+ val mimetype: String,
+ val filename: String,
+ var isPending: Boolean,
+ val viewType: Int = getViewTypeForMimeType(mimetype)
+) {
+ companion object {
+ fun getViewTypeForMimeType(mimetype: String): Int {
+ return when {
+ mimetype.isImageMimeType() || mimetype.isVideoMimeType() -> ATTACHMENT_MEDIA
+ mimetype.isVCardMimeType() -> ATTACHMENT_VCARD
+ else -> ATTACHMENT_DOCUMENT
+ }
+ }
+
+ fun areItemsTheSame(first: AttachmentSelection, second: AttachmentSelection): Boolean {
+ return first.uri == second.uri
+ }
+
+ fun areContentsTheSame(first: AttachmentSelection, second: AttachmentSelection): Boolean {
+ return first.uri == second.uri && first.mimetype == second.mimetype && first.filename == second.filename
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_image_vector.xml b/app/src/main/res/drawable/ic_image_vector.xml
new file mode 100644
index 00000000..0a221c91
--- /dev/null
+++ b/app/src/main/res/drawable/ic_image_vector.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_music_vector.xml b/app/src/main/res/drawable/ic_music_vector.xml
new file mode 100644
index 00000000..1a7c4d5b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_music_vector.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_vector_play_circle_outline.xml b/app/src/main/res/drawable/ic_vector_play_circle_outline.xml
new file mode 100644
index 00000000..8a252301
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vector_play_circle_outline.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_videocam_vector.xml b/app/src/main/res/drawable/ic_videocam_vector.xml
new file mode 100644
index 00000000..5ea7a412
--- /dev/null
+++ b/app/src/main/res/drawable/ic_videocam_vector.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_thread.xml b/app/src/main/res/layout/activity_thread.xml
index 4e3baf2c..7bdec389 100644
--- a/app/src/main/res/layout/activity_thread.xml
+++ b/app/src/main/res/layout/activity_thread.xml
@@ -22,10 +22,10 @@
-
@@ -34,6 +34,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
+ app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
+ android:importantForAccessibility="no"
+ app:layout_constraintBottom_toTopOf="@id/scheduled_message_holder"
+ app:layout_constraintTop_toBottomOf="@id/thread_messages_fastscroller"
+ tools:layout_height="1dp" />
+ android:src="@drawable/ic_plus_vector"
+ app:layout_constraintBottom_toTopOf="@id/attachment_picker_divider"
+ app:layout_constraintStart_toStartOf="parent" />
-
-
+
-
-
-
-
+ android:visibility="gone"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ app:layout_constraintBottom_toTopOf="@id/thread_type_message"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/scheduled_message_holder"
+ app:layout_goneMarginTop="@dimen/medium_margin"
+ tools:itemCount="2"
+ tools:listitem="@layout/item_attachment_document_preview"
+ tools:visibility="visible" />
+ android:minHeight="@dimen/normal_icon_size"
+ app:layout_constraintBottom_toTopOf="@id/attachment_picker_divider"
+ app:layout_constraintEnd_toStartOf="@id/thread_select_sim_icon"
+ app:layout_constraintStart_toEndOf="@+id/thread_add_attachment" />
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@id/attachment_picker_divider"
+ app:layout_constraintEnd_toStartOf="@id/thread_character_counter"
+ tools:visibility="visible" />
+ app:layout_constraintBottom_toBottomOf="@id/thread_select_sim_icon"
+ app:layout_constraintEnd_toEndOf="@id/thread_select_sim_icon"
+ app:layout_constraintStart_toStartOf="@id/thread_select_sim_icon"
+ app:layout_constraintTop_toTopOf="@id/thread_select_sim_icon"
+ tools:text="1"
+ tools:textColor="@color/dark_grey"
+ tools:visibility="visible" />
+ app:layout_constraintBottom_toTopOf="@id/attachment_picker_divider"
+ app:layout_constraintEnd_toStartOf="@+id/thread_send_message"
+ app:layout_constraintTop_toTopOf="@+id/thread_send_message"
+ tools:ignore="HardcodedText"
+ tools:visibility="visible" />
+ android:textSize="@dimen/smaller_text_size"
+ app:layout_constraintBottom_toTopOf="@id/attachment_picker_divider"
+ app:layout_constraintEnd_toEndOf="parent" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_attachment.xml b/app/src/main/res/layout/item_attachment.xml
deleted file mode 100644
index be794651..00000000
--- a/app/src/main/res/layout/item_attachment.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/item_attachment_document.xml b/app/src/main/res/layout/item_attachment_document.xml
new file mode 100644
index 00000000..603454b4
--- /dev/null
+++ b/app/src/main/res/layout/item_attachment_document.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_attachment_document_preview.xml b/app/src/main/res/layout/item_attachment_document_preview.xml
new file mode 100644
index 00000000..19f29d13
--- /dev/null
+++ b/app/src/main/res/layout/item_attachment_document_preview.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_attachment_media_preview.xml b/app/src/main/res/layout/item_attachment_media_preview.xml
new file mode 100644
index 00000000..c5a4c8df
--- /dev/null
+++ b/app/src/main/res/layout/item_attachment_media_preview.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_attachment_vcard.xml b/app/src/main/res/layout/item_attachment_vcard.xml
index 03ab668b..ff446f8f 100644
--- a/app/src/main/res/layout/item_attachment_vcard.xml
+++ b/app/src/main/res/layout/item_attachment_vcard.xml
@@ -1,57 +1,63 @@
-
+ app:layout_constraintTop_toTopOf="parent" />
-
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="@dimen/medium_margin"
+ android:orientation="vertical">
-
+
-
+
-
+
+
+
+
diff --git a/app/src/main/res/layout/item_attachment_vcard_preview.xml b/app/src/main/res/layout/item_attachment_vcard_preview.xml
new file mode 100644
index 00000000..ac469552
--- /dev/null
+++ b/app/src/main/res/layout/item_attachment_vcard_preview.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_remove_attachment_button.xml b/app/src/main/res/layout/item_remove_attachment_button.xml
new file mode 100644
index 00000000..b516b97b
--- /dev/null
+++ b/app/src/main/res/layout/item_remove_attachment_button.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_unknown_attachment.xml b/app/src/main/res/layout/item_unknown_attachment.xml
deleted file mode 100644
index 8b884084..00000000
--- a/app/src/main/res/layout/item_unknown_attachment.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e0959a28..0a03e197 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -3,9 +3,14 @@
72dp
64dp
36dp
- 60dp
+ 64dp
+ @dimen/attachment_preview_size
+ 156dp
24dp
15dp
64dp
20dp
+ 36dp
+ 80dp
+ 90dp