diff --git a/app/build.gradle b/app/build.gradle
index f208ea6e..46333b6d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -63,10 +63,10 @@ android {
}
dependencies {
- implementation 'com.github.SimpleMobileTools:Simple-Commons:0828fecd09'
+ implementation 'com.github.SimpleMobileTools:Simple-Commons:9162225f33'
implementation 'org.greenrobot:eventbus:3.3.1'
implementation 'com.github.tibbi:IndicatorFastScroll:4524cd0b61'
- implementation 'com.github.tibbi:android-smsmms:875a46a9c4'
+ implementation 'com.github.tibbi:android-smsmms:3581774c39'
implementation "me.leolin:ShortcutBadger:1.1.22"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b669ba54..e25bbf6b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -52,7 +52,8 @@
+ android:parentActivityName=".activities.MainActivity"
+ android:windowSoftInputMode="adjustResize" />
-
-
-
+
-
-
+
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
index 5b6641e2..22467a36 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
@@ -87,6 +87,7 @@ class MainActivity : SimpleActivity() {
override fun onResume() {
super.onResume()
setupToolbar(main_toolbar)
+
if (storedTextColor != getProperTextColor()) {
(conversations_list.adapter as? ConversationsAdapter)?.updateTextColor(getProperTextColor())
}
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 1ed6a1a2..331e8096 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
@@ -28,21 +27,15 @@ import android.view.Gravity
import android.view.KeyEvent
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.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.*
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
@@ -54,6 +47,7 @@ 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
@@ -61,8 +55,8 @@ import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.*
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 kotlinx.android.synthetic.main.layout_attachment_picker.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -73,12 +67,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
@@ -94,8 +82,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
@@ -106,6 +92,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)
@@ -146,6 +134,10 @@ class ThreadActivity : SimpleActivity() {
finish()
}
}
+
+ setupAttachmentPickerView()
+ setupKeyboardListener()
+ hideAttachmentPicker()
}
override fun onResume() {
@@ -162,17 +154,25 @@ class ThreadActivity : SimpleActivity() {
override fun onPause() {
super.onPause()
- if (thread_type_message.value != "" && attachmentSelections.isEmpty()) {
+ if (thread_type_message.value != "" && getAttachmentSelections().isEmpty()) {
saveSmsDraft(thread_type_message.value, threadId)
} else {
deleteSmsDraft(threadId)
}
bus?.post(Events.RefreshMessages())
-
isActivityVisible = false
}
+ override fun onBackPressed() {
+ isAttachmentPickerVisible = false
+ if (attachment_picker_holder.isVisible()) {
+ hideAttachmentPicker()
+ } else {
+ super.onBackPressed()
+ }
+ }
+
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
@@ -215,13 +215,16 @@ 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 && capturedImageUri != null) {
+ if (requestCode == CAPTURE_PHOTO_INTENT && capturedImageUri != null) {
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)
+ } else if (data != null) {
+ when (requestCode) {
+ CAPTURE_VIDEO_INTENT, PICK_DOCUMENT_INTENT, CAPTURE_AUDIO_INTENT, PICK_PHOTO_INTENT, PICK_VIDEO_INTENT -> addAttachment(data)
+ PICK_CONTACT_INTENT -> addContactAttachment(data)
+ PICK_SAVE_FILE_INTENT -> saveAttachment(resultData)
+ }
}
}
@@ -372,11 +375,13 @@ class ThreadActivity : SimpleActivity() {
}
}
- confirm_inserted_number?.setOnClickListener {
- val number = add_contact_or_number.value
- val phoneNumber = PhoneNumber(number, 0, "", number)
- val contact = SimpleContact(number.hashCode(), number.hashCode(), number, "", arrayListOf(phoneNumber), ArrayList(), ArrayList())
- addSelectedContact(contact)
+ runOnUiThread {
+ confirm_inserted_number?.setOnClickListener {
+ val number = add_contact_or_number.value
+ val phoneNumber = PhoneNumber(number, 0, "", number)
+ val contact = SimpleContact(number.hashCode(), number.hashCode(), number, "", arrayListOf(phoneNumber), ArrayList(), ArrayList())
+ addSelectedContact(contact)
+ }
}
}
@@ -428,6 +433,7 @@ class ThreadActivity : SimpleActivity() {
setTextColor(textColor)
compoundDrawables.forEach { it?.applyColorFilter(textColor) }
}
+
confirm_manage_contacts.applyColorFilter(textColor)
thread_add_attachment.applyColorFilter(textColor)
@@ -441,6 +447,7 @@ class ThreadActivity : SimpleActivity() {
thread_send_message.setOnClickListener {
sendMessage()
}
+
thread_send_message.setOnLongClickListener {
if (!isScheduledMessage) {
launchScheduleSendDialog()
@@ -499,7 +506,15 @@ class ThreadActivity : SimpleActivity() {
thread_type_message.setText(intent.getStringExtra(THREAD_TEXT))
thread_add_attachment.setOnClickListener {
- takeOrPickPhotoVideo()
+ if (attachment_picker_holder.isVisible()) {
+ isAttachmentPickerVisible = false
+ WindowCompat.getInsetsController(window, thread_type_message).show(WindowInsetsCompat.Type.ime())
+ } else {
+ isAttachmentPickerVisible = true
+ showOrHideAttachmentPicker()
+ WindowCompat.getInsetsController(window, thread_type_message).hide(WindowInsetsCompat.Type.ime())
+ }
+ window.decorView.requestApplyInsets()
}
if (intent.extras?.containsKey(THREAD_ATTACHMENT_URI) == true) {
@@ -796,135 +811,130 @@ 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 launchPickPhotoVideoIntent() {
- hideKeyboard()
- val mimeTypes = arrayOf("image/*", "video/*")
+ private fun getAttachmentsDir(): File {
+ return File(cacheDir, "attachments").apply {
+ if (!exists()) {
+ mkdirs()
+ }
+ }
+ }
+
+ private fun launchCapturePhotoIntent() {
+ val imageFile = File.createTempFile("attachment_", ".jpg", getAttachmentsDir())
+ 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 launchGetContentIntent(mimeTypes: Array, requestCode: Int) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
+ launchActivityForResult(this, requestCode)
+ }
+ }
- try {
- startActivityForResult(this, PICK_ATTACHMENT_INTENT)
- } catch (e: ActivityNotFoundException) {
- showErrorToast(getString(R.string.no_app_found))
- } catch (e: Exception) {
- showErrorToast(e)
+ private fun launchPickContactIntent() {
+ Intent(Intent.ACTION_PICK).apply {
+ type = ContactsContract.Contacts.CONTENT_TYPE
+ launchActivityForResult(this, PICK_CONTACT_INTENT)
+ }
+ }
+
+ private fun addContactAttachment(contactUri: Uri) {
+ ensureBackgroundThread {
+ val contact = ContactsHelper(this).getContactFromUri(contactUri)
+ if (contact != null) {
+ val outputFile = File(getAttachmentsDir(), "${contact.contactId}.vcf")
+ val outputStream = outputFile.outputStream()
+
+ VcfExporter().exportContacts(
+ activity = this,
+ outputStream = outputStream,
+ contacts = arrayListOf(contact),
+ showExportingToast = false,
+ ) {
+ if (it == VcfExporter.ExportResult.EXPORT_OK) {
+ val vCardUri = getMyFileUri(outputFile)
+ runOnUiThread {
+ addAttachment(vCardUri)
+ }
+ } else {
+ toast(R.string.unknown_error_occurred)
+ }
+ }
+ } else {
+ toast(R.string.unknown_error_occurred)
}
}
}
+ private fun getAttachmentsAdapter(): AttachmentsAdapter? {
+ val adapter = thread_attachments_recyclerview.adapter
+ return adapter as? AttachmentsAdapter
+ }
+
+ private fun getAttachmentSelections() = getAttachmentsAdapter()?.attachments ?: emptyList()
+
private fun addAttachment(uri: Uri) {
- val originalUriString = uri.toString()
- if (attachmentSelections.containsKey(originalUriString)) {
+ val id = uri.toString()
+ if (getAttachmentSelections().any { it.id == id }) {
+ toast(R.string.duplicate_item_warning)
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,
+ recyclerView = thread_attachments_recyclerview,
+ 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
- }
-
- private fun loadAttachmentPreview(attachmentView: View, uri: Uri) {
- if (isDestroyed || isFinishing) {
+ thread_attachments_recyclerview.beVisible()
+ val mimeType = contentResolver.getType(uri)
+ if (mimeType == null) {
+ toast(R.string.unknown_error_occurred)
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()
- }
+ val attachment = AttachmentSelection(
+ id = id,
+ uri = uri,
+ mimetype = mimeType,
+ filename = getFilenameFromUri(uri),
+ isPending = mimeType.isImageMimeType() && !mimeType.isGifMimeType()
+ )
+ adapter.addAttachment(attachment)
checkSendMessageAvailability()
}
@@ -949,7 +959,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() || (getAttachmentSelections().isNotEmpty() && !getAttachmentSelections().any { it.isPending })) {
thread_send_message.isEnabled = true
thread_send_message.isClickable = true
thread_send_message.alpha = 0.9f
@@ -963,7 +973,7 @@ class ThreadActivity : SimpleActivity() {
private fun sendMessage() {
var text = thread_type_message.value
- if (text.isEmpty() && attachmentSelections.isEmpty()) {
+ if (text.isEmpty() && getAttachmentSelections().isEmpty()) {
showErrorToast(getString(R.string.unknown_error_occurred))
return
}
@@ -1003,13 +1013,16 @@ class ThreadActivity : SimpleActivity() {
conversationsDB.insertOrUpdate(conversation.copy(date = nowSeconds))
}
scheduleMessage(message)
- }
- clearCurrentMessage()
- hideScheduleSendUi()
- scheduledMessage = null
- if (!refreshedSinceSent) {
- refreshMessages()
+ runOnUiThread {
+ clearCurrentMessage()
+ hideScheduleSendUi()
+ scheduledMessage = null
+
+ if (!refreshedSinceSent) {
+ refreshMessages()
+ }
+ }
}
} catch (e: Exception) {
showErrorToast(e.localizedMessage ?: getString(R.string.unknown_error_occurred))
@@ -1018,8 +1031,7 @@ class ThreadActivity : SimpleActivity() {
private fun sendNormalMessage(text: String, subscriptionId: Int) {
val addresses = participants.getAddresses()
- val attachments = attachmentSelections.values
- .map { it.uri }
+ val attachments = buildMessageAttachments()
try {
refreshedSinceSent = false
@@ -1038,9 +1050,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
@@ -1165,14 +1176,7 @@ class ThreadActivity : SimpleActivity() {
type = mimeType
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)
}
}
@@ -1220,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 getAttachmentSelections().isNotEmpty() || isGroupMms || isLongMmsMessage
}
private fun updateMessageType() {
@@ -1233,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)),
@@ -1298,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)
@@ -1365,7 +1360,7 @@ class ThreadActivity : SimpleActivity() {
read = false,
threadId = threadId,
isMMS = isMmsMessage(text),
- attachment = buildMessageAttachment(text, messageId),
+ attachment = MessageAttachment(messageId, text, buildMessageAttachments(messageId)),
senderName = "",
senderPhotoUri = "",
subscriptionId = subscriptionId,
@@ -1373,11 +1368,138 @@ class ThreadActivity : SimpleActivity() {
)
}
- private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment {
- val attachments = attachmentSelections.values
- .map { Attachment(null, messageId, it.uri.toString(), contentResolver.getType(it.uri) ?: "*/*", 0, 0, "") }
- .toArrayList()
+ private fun buildMessageAttachments(messageId: Long = -1L) = getAttachmentSelections()
+ .map { Attachment(null, messageId, it.uri.toString(), it.mimetype, 0, 0, it.filename) }
+ .toArrayList()
- return MessageAttachment(messageId, text, attachments)
+ private fun setupAttachmentPickerView() {
+ val buttonColors = arrayOf(
+ R.color.md_red_500,
+ R.color.md_brown_500,
+ R.color.md_pink_500,
+ R.color.md_purple_500,
+ R.color.md_teal_500,
+ R.color.md_green_500,
+ R.color.md_indigo_500,
+ R.color.md_blue_500
+ ).map { ResourcesCompat.getColor(resources, it, theme) }
+ arrayOf(
+ choose_photo_icon,
+ choose_video_icon,
+ take_photo_icon,
+ record_video_icon,
+ record_audio_icon,
+ pick_file_icon,
+ pick_contact_icon,
+ schedule_message_icon
+ ).forEachIndexed { index, icon ->
+ val iconColor = buttonColors[index]
+ icon.background.applyColorFilter(iconColor)
+ icon.applyColorFilter(iconColor.getContrastColor())
+ }
+
+ val textColor = getProperTextColor()
+ arrayOf(
+ choose_photo_text,
+ choose_video_text,
+ take_photo_text,
+ record_video_text,
+ record_audio_text,
+ pick_file_text,
+ pick_contact_text,
+ schedule_message_text
+ ).forEach { it.setTextColor(textColor) }
+
+ choose_photo.setOnClickListener {
+ launchGetContentIntent(arrayOf("image/*"), PICK_PHOTO_INTENT)
+ }
+ choose_video.setOnClickListener {
+ launchGetContentIntent(arrayOf("video/*"), PICK_VIDEO_INTENT)
+ }
+ take_photo.setOnClickListener {
+ launchCapturePhotoIntent()
+ }
+ record_video.setOnClickListener {
+ launchCaptureVideoIntent()
+ }
+ record_audio.setOnClickListener {
+ launchCaptureAudioIntent()
+ }
+ pick_file.setOnClickListener {
+ launchGetContentIntent(arrayOf("*/*"), PICK_DOCUMENT_INTENT)
+ }
+ 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() {
+ window.decorView.setOnApplyWindowInsetsListener { view, insets ->
+ showOrHideAttachmentPicker()
+ view.onApplyWindowInsets(insets)
+ }
+
+ val callback = object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
+ super.onPrepare(animation)
+ showOrHideAttachmentPicker()
+ }
+
+ override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList) = insets
+ }
+ ViewCompat.setWindowInsetsAnimationCallback(window.decorView, callback)
+ }
+
+ private fun showOrHideAttachmentPicker() {
+ val type = WindowInsetsCompat.Type.ime()
+ val insets = ViewCompat.getRootWindowInsets(window.decorView) ?: return
+ val isKeyboardVisible = insets.isVisible(type)
+
+ if (isKeyboardVisible) {
+ val keyboardHeight = insets.getInsets(type).bottom
+ val bottomBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
+
+ // check keyboard height just to be sure, 150 seems like a good middle ground between ime and navigation bar
+ config.keyboardHeight = if (keyboardHeight > 150) {
+ keyboardHeight - bottomBarHeight
+ } else {
+ getDefaultKeyboardHeight()
+ }
+ hideAttachmentPicker()
+ } else if (isAttachmentPickerVisible) {
+ showAttachmentPicker()
+ }
}
}
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..adad4bb1
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt
@@ -0,0 +1,207 @@
+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.R
+import com.simplemobiletools.smsmessenger.activities.VCardViewerActivity
+import com.simplemobiletools.smsmessenger.extensions.*
+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 recyclerView: RecyclerView,
+ 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()
+
+ override fun getItemViewType(position: Int): Int {
+ return getItem(position).viewType
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val layoutRes = when (viewType) {
+ ATTACHMENT_DOCUMENT -> R.layout.item_attachment_document_preview
+ ATTACHMENT_VCARD -> R.layout.item_attachment_vcard_preview
+ ATTACHMENT_MEDIA -> 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() { view, _ ->
+ when (attachment.viewType) {
+ ATTACHMENT_DOCUMENT -> {
+ view.setupDocumentPreview(
+ uri = attachment.uri,
+ title = attachment.filename,
+ mimeType = attachment.mimetype,
+ 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)
+ }
+ }
+ }
+
+ fun clear() {
+ attachments.clear()
+ submitList(emptyList())
+ recyclerView.onGlobalLayout {
+ 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()) {
+ clear()
+ } else {
+ submitList(attachments.toList())
+ }
+ }
+
+ 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)
+ }
+ }
+
+ val compressImage = attachment.mimetype.isImageMimeType() && !attachment.mimetype.isGifMimeType()
+ if (compressImage && 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(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(R.dimen.activity_margin).toInt()
+ val size = resources.getDimension(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(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(callback: (itemView: View, adapterPosition: Int) -> Unit): View {
+ return itemView.apply {
+ callback(this, adapterPosition)
+ }
+ }
+ }
+}
+
+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 479e14a2..63a484d3 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,12 @@
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.Size
import android.util.TypedValue
import android.view.Menu
import android.view.View
@@ -38,20 +37,16 @@ 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
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_holder
import kotlinx.android.synthetic.main.item_received_message.view.thread_message_play_outline
-import kotlinx.android.synthetic.main.item_received_unknown_attachment.view.*
import kotlinx.android.synthetic.main.item_sent_message.view.*
-import kotlinx.android.synthetic.main.item_sent_unknown_attachment.view.*
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 java.util.*
class ThreadAdapter(
activity: SimpleActivity, var messages: ArrayList, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit
@@ -60,6 +55,7 @@ class ThreadAdapter(
@SuppressLint("MissingPermission")
private val hasMultipleSIMCards = (activity.subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1
+ private val maxChatBubbleWidth = activity.usableScreenSize.x * 0.8f
init {
setupDragListener(true)
@@ -286,12 +282,10 @@ class ThreadAdapter(
if (message.attachment?.attachments?.isNotEmpty() == true) {
for (attachment in message.attachment.attachments) {
val mimetype = attachment.mimetype
- if (mimetype.isImageMimeType() || mimetype.startsWith("video/")) {
- setupImageView(holder, view, message, attachment)
- } else if (mimetype.isVCardMimeType()) {
- setupVCardView(holder, view, message, attachment)
- } else {
- setupFileView(holder, view, message, attachment)
+ when {
+ mimetype.isImageMimeType() || mimetype.isVideoMimeType() -> setupImageView(holder, view, message, attachment)
+ mimetype.isVCardMimeType() -> setupVCardView(holder, view, message, attachment)
+ else -> setupFileView(holder, view, message, attachment)
}
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
@@ -374,14 +368,20 @@ class ThreadAdapter(
return false
}
- override fun onResourceReady(dr: Drawable?, a: Any?, t: Target?, d: DataSource?, i: Boolean) =
- false
+ override fun onResourceReady(dr: Drawable?, a: Any?, t: Target?, d: DataSource?, i: Boolean) = false
})
+ // limit attachment sizes to avoid causing OOM
+ var wantedAttachmentSize = Size(attachment.width, attachment.height)
+ if (wantedAttachmentSize.width > maxChatBubbleWidth) {
+ val newHeight = wantedAttachmentSize.height / (wantedAttachmentSize.width / maxChatBubbleWidth)
+ wantedAttachmentSize = Size(maxChatBubbleWidth.toInt(), newHeight.toInt())
+ }
+
builder = if (isTallImage) {
- builder.override(attachment.width, attachment.width)
+ builder.override(wantedAttachmentSize.width, wantedAttachmentSize.width)
} else {
- builder.override(attachment.width, attachment.height)
+ builder.override(wantedAttachmentSize.width, wantedAttachmentSize.height)
}
try {
@@ -393,7 +393,7 @@ class ThreadAdapter(
if (actModeCallback.isSelectable) {
holder.viewClicked(message)
} else {
- launchViewIntent(uri, mimetype, attachment.filename)
+ activity.launchViewIntent(uri, mimetype, attachment.filename)
}
}
imageView.setOnLongClickListener {
@@ -407,50 +407,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
- }
- }
- }
- }
}
}
@@ -458,73 +431,22 @@ class ThreadAdapter(
val mimetype = attachment.mimetype
val uri = attachment.getUri()
parent.apply {
- if (message.isReceivedMessage()) {
- val attachmentView = layoutInflater.inflate(R.layout.item_received_unknown_attachment, null).apply {
- thread_received_attachment_label.apply {
- if (attachment.filename.isNotEmpty()) {
- thread_received_attachment_label.text = attachment.filename
+ val attachmentView = layoutInflater.inflate(R.layout.item_attachment_document, null).apply {
+ setupDocumentPreview(
+ uri = uri,
+ title = attachment.filename,
+ mimeType = attachment.mimetype,
+ onClick = {
+ if (actModeCallback.isSelectable) {
+ holder.viewClicked(message)
+ } else {
+ activity.launchViewIntent(uri, mimetype, attachment.filename)
}
- setTextColor(textColor)
- setOnClickListener {
- if (actModeCallback.isSelectable) {
- holder.viewClicked(message)
- } else {
- launchViewIntent(uri, mimetype, attachment.filename)
- }
- }
- setOnLongClickListener {
- holder.viewLongClicked()
- true
- }
- }
- }
- thread_mesage_attachments_holder.addView(attachmentView)
- } else {
- val background = context.getProperPrimaryColor()
- val attachmentView = layoutInflater.inflate(R.layout.item_sent_unknown_attachment, null).apply {
- thread_sent_attachment_label.apply {
- this.background.applyColorFilter(background)
- setTextColor(background.getContrastColor())
- if (attachment.filename.isNotEmpty()) {
- thread_sent_attachment_label.text = attachment.filename
- }
- setOnClickListener {
- if (actModeCallback.isSelectable) {
- holder.viewClicked(message)
- } else {
- launchViewIntent(uri, mimetype, attachment.filename)
- }
- }
- setOnLongClickListener {
- holder.viewLongClicked()
- true
- }
- }
- }
- 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)
+ },
+ onLongClick = { holder.viewLongClicked() },
+ )
}
+ thread_mesage_attachments_holder.addView(attachmentView)
}
}
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..6b97672f 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
@@ -35,6 +35,7 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.ThreadActivity
import com.simplemobiletools.smsmessenger.databases.MessagesDatabase
import com.simplemobiletools.smsmessenger.helpers.*
+import com.simplemobiletools.smsmessenger.helpers.AttachmentUtils.parseAttachmentNames
import com.simplemobiletools.smsmessenger.interfaces.AttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao
import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
@@ -307,7 +308,8 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
val selectionArgs = arrayOf(id.toString())
val messageAttachment = MessageAttachment(id, "", arrayListOf())
- var attachmentName = ""
+ var attachmentNames: List? = null
+ var attachmentCount = 0
queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
val partId = cursor.getLongValue(Mms._ID)
val mimetype = cursor.getStringValue(Mms.Part.CONTENT_TYPE)
@@ -332,14 +334,13 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
val attachment = Attachment(partId, id, fileUri.toString(), mimetype, width, height, "")
messageAttachment.attachments.add(attachment)
} else if (mimetype != "application/smil") {
+ val attachmentName = attachmentNames?.getOrNull(attachmentCount) ?: ""
val attachment = Attachment(partId, id, Uri.withAppendedPath(uri, partId.toString()).toString(), mimetype, 0, 0, attachmentName)
messageAttachment.attachments.add(attachment)
+ attachmentCount++
} else {
val text = cursor.getStringValue(Mms.Part.TEXT)
- val cutName = text.substringAfter("ref src=\"").substringBefore("\"")
- if (cutName.isNotEmpty()) {
- attachmentName = cutName
- }
+ attachmentNames = parseAttachmentNames(text)
}
}
@@ -1064,3 +1065,5 @@ fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List
return
}
}
+
+fun Context.getDefaultKeyboardHeight() = resources.getDimensionPixelSize(R.dimen.default_keyboard_height)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt
index 89fb0f03..8ad4bd49 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/String.kt
@@ -15,7 +15,35 @@ fun String.isImageMimeType(): Boolean {
return lowercase().startsWith("image")
}
+fun String.isGifMimeType(): Boolean {
+ return lowercase().endsWith("gif")
+}
+
+fun String.isVideoMimeType(): Boolean {
+ return lowercase().startsWith("video")
+}
+
fun String.isVCardMimeType(): Boolean {
val lowercase = lowercase()
return lowercase.endsWith("x-vcard") || lowercase.endsWith("vcard")
}
+
+fun String.isAudioMimeType(): Boolean {
+ return lowercase().startsWith("audio")
+}
+
+fun String.isCalendarMimeType(): Boolean {
+ return lowercase().endsWith("calendar")
+}
+
+fun String.isPdfMimeType(): Boolean {
+ return lowercase().endsWith("pdf")
+}
+
+fun String.isZipMimeType(): Boolean {
+ return lowercase().endsWith("zip")
+}
+
+fun String.isPlainTextMimeType(): Boolean {
+ return lowercase() == "text/plain"
+}
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/AttachmentPreviews.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt
new file mode 100644
index 00000000..82d7eeb8
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentPreviews.kt
@@ -0,0 +1,152 @@
+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 com.simplemobiletools.smsmessenger.extensions.*
+import kotlinx.android.synthetic.main.item_attachment_document.view.*
+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.setupDocumentPreview(
+ uri: Uri,
+ title: String,
+ mimeType: String,
+ attachment: Boolean = false,
+ onClick: (() -> Unit)? = null,
+ onLongClick: (() -> Unit)? = null,
+ onRemoveButtonClicked: (() -> Unit)? = null
+) {
+ if (title.isNotEmpty()) {
+ filename.text = title
+ }
+
+ try {
+ val size = context.getFileSizeFromUri(uri)
+ file_size.beVisible()
+ file_size.text = size.formatSize()
+ } catch (e: Exception) {
+ file_size.beGone()
+ }
+
+ val textColor = context.getProperTextColor()
+ val primaryColor = context.getProperPrimaryColor()
+
+ document_attachment_holder.background.applyColorFilter(textColor)
+ filename.setTextColor(textColor)
+ file_size.setTextColor(textColor)
+
+ icon.setImageResource(getIconResourceForMimeType(mimeType))
+ 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
+ }
+}
+
+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 ->
+ activity.runOnUiThread {
+ if (vCards.isEmpty()) {
+ vcard_title.beVisible()
+ vcard_title.text = context.getString(R.string.unknown_error_occurred)
+ return@runOnUiThread
+ }
+ val title = vCards.firstOrNull()?.parseNameFromVCard()
+ val imageIcon = if (title != null) {
+ SimpleContactsHelper(activity).getContactLetterIcon(title)
+ } else {
+ null
+ }
+
+ 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
+ }
+ }
+ }
+}
+
+private fun getIconResourceForMimeType(mimeType: String) = when {
+ mimeType.isAudioMimeType() -> R.drawable.ic_vector_audio_file
+ mimeType.isCalendarMimeType() -> R.drawable.ic_calendar_month_vector
+ mimeType.isPdfMimeType() -> R.drawable.ic_vector_pdf
+ mimeType.isZipMimeType() -> R.drawable.ic_vector_folder_zip
+ else -> R.drawable.ic_document_vector
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentUtils.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentUtils.kt
new file mode 100644
index 00000000..1b062c86
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/AttachmentUtils.kt
@@ -0,0 +1,74 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import android.util.Xml
+import org.xmlpull.v1.XmlPullParser
+
+object AttachmentUtils {
+
+ fun parseAttachmentNames(text: String): List {
+ val parser = Xml.newPullParser()
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+ parser.setInput(text.reader())
+ parser.nextTag()
+ return readSmil(parser)
+ }
+
+ private fun readSmil(parser: XmlPullParser): List {
+ parser.require(XmlPullParser.START_TAG, null, "smil")
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ if (parser.name == "body") {
+ return readBody(parser)
+ } else {
+ skip(parser)
+ }
+ }
+
+ return emptyList()
+ }
+
+ private fun readBody(parser: XmlPullParser): List {
+ val names = mutableListOf()
+ parser.require(XmlPullParser.START_TAG, null, "body")
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ if (parser.name == "par") {
+ parser.require(XmlPullParser.START_TAG, null, "par")
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ if (parser.name == "ref") {
+ val value = parser.getAttributeValue(null, "src")
+ names.add(value)
+ parser.nextTag()
+ }
+ }
+ } else {
+ skip(parser)
+ }
+ }
+ return names
+ }
+
+ private fun skip(parser: XmlPullParser) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ throw IllegalStateException()
+ }
+
+ var depth = 1
+ while (depth != 0) {
+ when (parser.next()) {
+ XmlPullParser.END_TAG -> depth--
+ XmlPullParser.START_TAG -> depth++
+ }
+ }
+ }
+}
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 b747709f..799e8ca5 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
@@ -2,6 +2,7 @@ package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import com.simplemobiletools.commons.helpers.BaseConfig
+import com.simplemobiletools.smsmessenger.extensions.getDefaultKeyboardHeight
import com.simplemobiletools.smsmessenger.models.Conversation
class Config(context: Context) : BaseConfig(context) {
@@ -90,4 +91,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, context.getDefaultKeyboardHeight())
+ set(keyboardHeight) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, keyboardHeight).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 39389bf2..45c41f48 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
@@ -31,6 +31,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"
@@ -46,6 +47,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
@@ -61,6 +67,16 @@ const val FILE_SIZE_2_MB = 2_097_152L
const val MESSAGES_LIMIT = 50
+// intent launch request codes
+const val PICK_PHOTO_INTENT = 42
+const val PICK_VIDEO_INTENT = 49
+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/ContactsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt
new file mode 100644
index 00000000..0bfc46db
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt
@@ -0,0 +1,449 @@
+package com.simplemobiletools.smsmessenger.helpers
+
+import android.content.Context
+import android.net.Uri
+import android.provider.ContactsContract
+import android.provider.ContactsContract.CommonDataKinds.*
+import android.provider.ContactsContract.Data
+import android.text.TextUtils
+import android.util.SparseArray
+import com.simplemobiletools.commons.extensions.*
+import com.simplemobiletools.commons.models.PhoneNumber
+import com.simplemobiletools.commons.models.contacts.*
+import com.simplemobiletools.commons.models.contacts.Email
+import com.simplemobiletools.commons.models.contacts.Event
+import com.simplemobiletools.commons.models.contacts.Organization
+import com.simplemobiletools.commons.overloads.times
+
+// based on the ContactsHelper from Simple-Contacts
+class ContactsHelper(val context: Context) {
+ private var displayContactSources = ArrayList()
+
+ fun getContactFromUri(uri: Uri): Contact? {
+ val key = getLookupKeyFromUri(uri) ?: return null
+ return getContactWithLookupKey(key)
+ }
+
+ private fun getLookupKeyFromUri(lookupUri: Uri): String? {
+ val projection = arrayOf(ContactsContract.Contacts.LOOKUP_KEY)
+ val cursor = context.contentResolver.query(lookupUri, projection, null, null, null)
+ cursor?.use {
+ if (cursor.moveToFirst()) {
+ return cursor.getStringValue(ContactsContract.Contacts.LOOKUP_KEY)
+ }
+ }
+ return null
+ }
+
+ private fun getContactWithLookupKey(key: String): Contact? {
+ val selection = "(${Data.MIMETYPE} = ? OR ${Data.MIMETYPE} = ?) AND ${Data.LOOKUP_KEY} = ?"
+ val selectionArgs = arrayOf(StructuredName.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, key)
+ return parseContactCursor(selection, selectionArgs)
+ }
+
+ private fun parseContactCursor(selection: String, selectionArgs: Array): Contact? {
+ val storedGroups = getDeviceStoredGroups()
+ val uri = Data.CONTENT_URI
+ val projection = getContactProjection()
+
+ val cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
+ cursor?.use {
+ if (cursor.moveToFirst()) {
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+
+ var prefix = ""
+ var firstName = ""
+ var middleName = ""
+ var surname = ""
+ var suffix = ""
+ val mimetype = cursor.getStringValue(Data.MIMETYPE)
+
+ // ignore names at Organization type contacts
+ if (mimetype == StructuredName.CONTENT_ITEM_TYPE) {
+ prefix = cursor.getStringValue(StructuredName.PREFIX) ?: ""
+ firstName = cursor.getStringValue(StructuredName.GIVEN_NAME) ?: ""
+ middleName = cursor.getStringValue(StructuredName.MIDDLE_NAME) ?: ""
+ surname = cursor.getStringValue(StructuredName.FAMILY_NAME) ?: ""
+ suffix = cursor.getStringValue(StructuredName.SUFFIX) ?: ""
+ }
+
+ val nickname = getNicknames(id)[id] ?: ""
+ val photoUri = cursor.getStringValue(Phone.PHOTO_URI) ?: ""
+ val number = getPhoneNumbers(id)[id] ?: ArrayList()
+ val emails = getEmails(id)[id] ?: ArrayList()
+ val addresses = getAddresses(id)[id] ?: ArrayList()
+ val events = getEvents(id)[id] ?: ArrayList()
+ val notes = getNotes(id)[id] ?: ""
+ val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: ""
+ val starred = cursor.getIntValue(StructuredName.STARRED)
+ val ringtone = cursor.getStringValue(StructuredName.CUSTOM_RINGTONE)
+ val contactId = cursor.getIntValue(Data.CONTACT_ID)
+ val groups = getContactGroups(storedGroups, contactId)[contactId] ?: ArrayList()
+ val thumbnailUri = cursor.getStringValue(StructuredName.PHOTO_THUMBNAIL_URI) ?: ""
+ val organization = getOrganizations(id)[id] ?: Organization("", "")
+ val websites = getWebsites(id)[id] ?: ArrayList()
+ val ims = getIMs(id)[id] ?: ArrayList()
+ return Contact(
+ id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, number, emails, addresses, events,
+ accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, ims, mimetype, ringtone
+ )
+ }
+ }
+
+ return null
+ }
+
+ private fun getPhoneNumbers(contactId: Int? = null): SparseArray> {
+ val phoneNumbers = SparseArray>()
+ val uri = Phone.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ Phone.NUMBER,
+ Phone.NORMALIZED_NUMBER,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.IS_PRIMARY
+ )
+
+ val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?"
+ val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString())
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val number = cursor.getStringValue(Phone.NUMBER) ?: return@queryCursor
+ val normalizedNumber = cursor.getStringValue(Phone.NORMALIZED_NUMBER) ?: number.normalizePhoneNumber()
+ val type = cursor.getIntValue(Phone.TYPE)
+ val label = cursor.getStringValue(Phone.LABEL) ?: ""
+ val isPrimary = cursor.getIntValue(Phone.IS_PRIMARY) != 0
+
+ if (phoneNumbers[id] == null) {
+ phoneNumbers.put(id, ArrayList())
+ }
+
+ val phoneNumber = PhoneNumber(number, type, label, normalizedNumber, isPrimary)
+ phoneNumbers[id].add(phoneNumber)
+ }
+
+ return phoneNumbers
+ }
+
+ private fun getNicknames(contactId: Int? = null): SparseArray {
+ val nicknames = SparseArray()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ Nickname.NAME
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(Nickname.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val nickname = cursor.getStringValue(Nickname.NAME) ?: return@queryCursor
+ nicknames.put(id, nickname)
+ }
+
+ return nicknames
+ }
+
+ private fun getEmails(contactId: Int? = null): SparseArray> {
+ val emails = SparseArray>()
+ val uri = ContactsContract.CommonDataKinds.Email.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ ContactsContract.CommonDataKinds.Email.DATA,
+ ContactsContract.CommonDataKinds.Email.TYPE,
+ ContactsContract.CommonDataKinds.Email.LABEL
+ )
+
+ val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?"
+ val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString())
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val email = cursor.getStringValue(ContactsContract.CommonDataKinds.Email.DATA) ?: return@queryCursor
+ val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Email.TYPE)
+ val label = cursor.getStringValue(ContactsContract.CommonDataKinds.Email.LABEL) ?: ""
+
+ if (emails[id] == null) {
+ emails.put(id, ArrayList())
+ }
+
+ emails[id]!!.add(Email(email, type, label))
+ }
+
+ return emails
+ }
+
+ private fun getAddresses(contactId: Int? = null): SparseArray> {
+ val addresses = SparseArray>()
+ val uri = StructuredPostal.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ StructuredPostal.FORMATTED_ADDRESS,
+ StructuredPostal.TYPE,
+ StructuredPostal.LABEL
+ )
+
+ val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?"
+ val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString())
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val address = cursor.getStringValue(StructuredPostal.FORMATTED_ADDRESS) ?: return@queryCursor
+ val type = cursor.getIntValue(StructuredPostal.TYPE)
+ val label = cursor.getStringValue(StructuredPostal.LABEL) ?: ""
+
+ if (addresses[id] == null) {
+ addresses.put(id, ArrayList())
+ }
+
+ addresses[id]!!.add(Address(address, type, label))
+ }
+
+ return addresses
+ }
+
+ private fun getIMs(contactId: Int? = null): SparseArray> {
+ val IMs = SparseArray>()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ Im.DATA,
+ Im.PROTOCOL,
+ Im.CUSTOM_PROTOCOL
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(Im.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val IM = cursor.getStringValue(Im.DATA) ?: return@queryCursor
+ val type = cursor.getIntValue(Im.PROTOCOL)
+ val label = cursor.getStringValue(Im.CUSTOM_PROTOCOL) ?: ""
+
+ if (IMs[id] == null) {
+ IMs.put(id, ArrayList())
+ }
+
+ IMs[id]!!.add(IM(IM, type, label))
+ }
+
+ return IMs
+ }
+
+ private fun getEvents(contactId: Int? = null): SparseArray> {
+ val events = SparseArray>()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ ContactsContract.CommonDataKinds.Event.START_DATE,
+ ContactsContract.CommonDataKinds.Event.TYPE
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val startDate = cursor.getStringValue(ContactsContract.CommonDataKinds.Event.START_DATE) ?: return@queryCursor
+ val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Event.TYPE)
+
+ if (events[id] == null) {
+ events.put(id, ArrayList())
+ }
+
+ events[id]!!.add(Event(startDate, type))
+ }
+
+ return events
+ }
+
+ private fun getNotes(contactId: Int? = null): SparseArray {
+ val notes = SparseArray()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ Note.NOTE
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(Note.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val note = cursor.getStringValue(Note.NOTE) ?: return@queryCursor
+ notes.put(id, note)
+ }
+
+ return notes
+ }
+
+ private fun getOrganizations(contactId: Int? = null): SparseArray {
+ val organizations = SparseArray()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ ContactsContract.CommonDataKinds.Organization.COMPANY,
+ ContactsContract.CommonDataKinds.Organization.TITLE
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val company = cursor.getStringValue(ContactsContract.CommonDataKinds.Organization.COMPANY) ?: ""
+ val title = cursor.getStringValue(ContactsContract.CommonDataKinds.Organization.TITLE) ?: ""
+ if (company.isEmpty() && title.isEmpty()) {
+ return@queryCursor
+ }
+
+ val organization = Organization(company, title)
+ organizations.put(id, organization)
+ }
+
+ return organizations
+ }
+
+ private fun getWebsites(contactId: Int? = null): SparseArray> {
+ val websites = SparseArray>()
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.RAW_CONTACT_ID,
+ Website.URL
+ )
+
+ val selection = getSourcesSelection(true, contactId != null)
+ val selectionArgs = getSourcesSelectionArgs(Website.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.RAW_CONTACT_ID)
+ val url = cursor.getStringValue(Website.URL) ?: return@queryCursor
+
+ if (websites[id] == null) {
+ websites.put(id, ArrayList())
+ }
+
+ websites[id]!!.add(url)
+ }
+
+ return websites
+ }
+
+ private fun getDeviceStoredGroups(): java.util.ArrayList {
+ val groups = java.util.ArrayList()
+
+ val uri = ContactsContract.Groups.CONTENT_URI
+ val projection = arrayOf(
+ ContactsContract.Groups._ID,
+ ContactsContract.Groups.TITLE,
+ ContactsContract.Groups.SYSTEM_ID
+ )
+
+ val selection = "${ContactsContract.Groups.AUTO_ADD} = ? AND ${ContactsContract.Groups.FAVORITES} = ?"
+ val selectionArgs = arrayOf("0", "0")
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getLongValue(ContactsContract.Groups._ID)
+ val title = cursor.getStringValue(ContactsContract.Groups.TITLE) ?: return@queryCursor
+
+ val systemId = cursor.getStringValue(ContactsContract.Groups.SYSTEM_ID)
+ if (groups.map { it.title }.contains(title) && systemId != null) {
+ return@queryCursor
+ }
+
+ groups.add(Group(id, title))
+ }
+ return groups
+ }
+
+ private fun getContactGroups(storedGroups: ArrayList, contactId: Int? = null): SparseArray> {
+ val groups = SparseArray>()
+
+ val uri = Data.CONTENT_URI
+ val projection = arrayOf(
+ Data.CONTACT_ID,
+ Data.DATA1
+ )
+
+ val selection = getSourcesSelection(true, contactId != null, false)
+ val selectionArgs = getSourcesSelectionArgs(GroupMembership.CONTENT_ITEM_TYPE, contactId)
+
+ context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor ->
+ val id = cursor.getIntValue(Data.CONTACT_ID)
+ val newRowId = cursor.getLongValue(Data.DATA1)
+
+ val groupTitle = storedGroups.firstOrNull { it.id == newRowId }?.title ?: return@queryCursor
+ val group = Group(newRowId, groupTitle)
+ if (groups[id] == null) {
+ groups.put(id, ArrayList())
+ }
+ groups[id]!!.add(group)
+ }
+
+ return groups
+ }
+
+ private fun getQuestionMarks() = ("?," * displayContactSources.filter { it.isNotEmpty() }.size).trimEnd(',')
+
+ private fun getSourcesSelection(addMimeType: Boolean = false, addContactId: Boolean = false, useRawContactId: Boolean = true): String {
+ val strings = ArrayList()
+ if (addMimeType) {
+ strings.add("${Data.MIMETYPE} = ?")
+ }
+
+ if (addContactId) {
+ strings.add("${if (useRawContactId) Data.RAW_CONTACT_ID else Data.CONTACT_ID} = ?")
+ } else {
+ // sometimes local device storage has null account_name, handle it properly
+ val accountNameString = StringBuilder()
+ if (displayContactSources.contains("")) {
+ accountNameString.append("(")
+ }
+ accountNameString.append("${ContactsContract.RawContacts.ACCOUNT_NAME} IN (${getQuestionMarks()})")
+ if (displayContactSources.contains("")) {
+ accountNameString.append(" OR ${ContactsContract.RawContacts.ACCOUNT_NAME} IS NULL)")
+ }
+ strings.add(accountNameString.toString())
+ }
+
+ return TextUtils.join(" AND ", strings)
+ }
+
+ private fun getSourcesSelectionArgs(mimetype: String? = null, contactId: Int? = null): Array {
+ val args = ArrayList()
+
+ if (mimetype != null) {
+ args.add(mimetype)
+ }
+
+ if (contactId != null) {
+ args.add(contactId.toString())
+ } else {
+ args.addAll(displayContactSources.filter { it.isNotEmpty() })
+ }
+
+ return args.toTypedArray()
+ }
+
+ private fun getContactProjection() = arrayOf(
+ Data.MIMETYPE,
+ Data.CONTACT_ID,
+ Data.RAW_CONTACT_ID,
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX,
+ StructuredName.PHOTO_URI,
+ StructuredName.PHOTO_THUMBNAIL_URI,
+ StructuredName.STARRED,
+ StructuredName.CUSTOM_RINGTONE,
+ ContactsContract.RawContacts.ACCOUNT_NAME,
+ ContactsContract.RawContacts.ACCOUNT_TYPE
+ )
+
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt
index fb048c6c..6935744d 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt
@@ -4,7 +4,6 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.core.app.AlarmManagerCompat
@@ -15,6 +14,8 @@ import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.isMarshmallowPlus
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
+import com.simplemobiletools.smsmessenger.extensions.isPlainTextMimeType
+import com.simplemobiletools.smsmessenger.models.Attachment
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.receivers.ScheduledMessageReceiver
import com.simplemobiletools.smsmessenger.receivers.SmsStatusDeliveredReceiver
@@ -34,7 +35,7 @@ fun Context.getSendMessageSettings(): Settings {
return settings
}
-fun Context.sendMessage(text: String, addresses: List, subscriptionId: Int?, attachments: List) {
+fun Context.sendMessage(text: String, addresses: List, subscriptionId: Int?, attachments: List) {
val settings = getSendMessageSettings()
if (subscriptionId != null) {
settings.subscriptionId = subscriptionId
@@ -44,11 +45,19 @@ fun Context.sendMessage(text: String, addresses: List, subscriptionId: I
val message = com.klinker.android.send_message.Message(text, addresses.toTypedArray())
if (attachments.isNotEmpty()) {
- for (uri in attachments) {
+ for (attachment in attachments) {
try {
- val byteArray = contentResolver.openInputStream(uri)?.readBytes() ?: continue
- val mimeType = contentResolver.getType(uri) ?: continue
- message.addMedia(byteArray, mimeType)
+ val uri = attachment.getUri()
+ contentResolver.openInputStream(uri)?.use {
+ val bytes = it.readBytes()
+ val mimeType = if (attachment.mimetype.isPlainTextMimeType()) {
+ "application/txt"
+ } else {
+ attachment.mimetype
+ }
+ val name = attachment.filename
+ message.addMedia(bytes, mimeType, name, name)
+ }
} catch (e: Exception) {
showErrorToast(e)
} catch (e: Error) {
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardParser.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardParser.kt
index 3456c080..d93c36ac 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardParser.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/VCardParser.kt
@@ -8,7 +8,12 @@ import ezvcard.VCard
fun parseVCardFromUri(context: Context, uri: Uri, callback: (vCards: List) -> Unit) {
ensureBackgroundThread {
- val inputStream = context.contentResolver.openInputStream(uri)
+ val inputStream = try {
+ context.contentResolver.openInputStream(uri)
+ } catch (e: Exception) {
+ callback(emptyList())
+ return@ensureBackgroundThread
+ }
val vCards = Ezvcard.parse(inputStream).all()
callback(vCards)
}
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..dc92e584 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,36 @@
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 id: String,
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.id == second.id
+ }
+
+ 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/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
index 18d97921..8c2acf0b 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsReceiver.kt
@@ -10,6 +10,7 @@ import com.simplemobiletools.commons.extensions.normalizePhoneNumber
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.*
+import com.simplemobiletools.smsmessenger.helpers.refreshMessages
// more info at https://github.com/klinker41/android-smsmms
class MmsReceiver : com.klinker.android.send_message.MmsReceivedReceiver() {
@@ -42,6 +43,7 @@ class MmsReceiver : com.klinker.android.send_message.MmsReceivedReceiver() {
ensureBackgroundThread {
context.conversationsDB.insertOrUpdate(conversation)
context.updateUnreadCountBadge(context.conversationsDB.getUnreadConversations())
+ refreshMessages()
}
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt
index bc89a283..53c9e020 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt
@@ -40,7 +40,7 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
}
val addresses = message.participants.getAddresses()
- val attachments = message.attachment?.attachments?.mapNotNull { it.getUri() } ?: emptyList()
+ val attachments = message.attachment?.attachments ?: emptyList()
try {
context.sendMessage(message.body, addresses, message.subscriptionId, attachments)
diff --git a/app/src/main/res/drawable/ic_document_vector.xml b/app/src/main/res/drawable/ic_document_vector.xml
new file mode 100644
index 00000000..d2fae544
--- /dev/null
+++ b/app/src/main/res/drawable/ic_document_vector.xml
@@ -0,0 +1,3 @@
+
+
+
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..b20b8ad6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_image_vector.xml
@@ -0,0 +1,3 @@
+
+
+
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..0c4e948d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_music_vector.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_vector_audio_file.xml b/app/src/main/res/drawable/ic_vector_audio_file.xml
new file mode 100644
index 00000000..4705cfa2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vector_audio_file.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_vector_folder_zip.xml b/app/src/main/res/drawable/ic_vector_folder_zip.xml
new file mode 100644
index 00000000..1a4b5f79
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vector_folder_zip.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_vector_pdf.xml b/app/src/main/res/drawable/ic_vector_pdf.xml
new file mode 100644
index 00000000..23b0a48b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vector_pdf.xml
@@ -0,0 +1,3 @@
+
+
+
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..6003cd60
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vector_play_circle_outline.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_video_camera_vector.xml b/app/src/main/res/drawable/ic_video_camera_vector.xml
new file mode 100644
index 00000000..872944c0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_video_camera_vector.xml
@@ -0,0 +1,3 @@
+
+
+
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..7b16409b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_videocam_vector.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/drawable/linear_layout_vertical_divider.xml b/app/src/main/res/drawable/linear_layout_vertical_divider.xml
new file mode 100644
index 00000000..ceb1be00
--- /dev/null
+++ b/app/src/main/res/drawable/linear_layout_vertical_divider.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_thread.xml b/app/src/main/res/layout/activity_thread.xml
index 4e3baf2c..42448012 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_received_message.xml b/app/src/main/res/layout/item_received_message.xml
index c36e4b8c..409f6c18 100644
--- a/app/src/main/res/layout/item_received_message.xml
+++ b/app/src/main/res/layout/item_received_message.xml
@@ -34,7 +34,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/thread_message_sender_photo"
- android:orientation="vertical" />
+ android:divider="@drawable/linear_layout_vertical_divider"
+ android:orientation="vertical"
+ android:showDividers="middle" />
-
-
-
-
-
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_sent_message.xml b/app/src/main/res/layout/item_sent_message.xml
index def52b64..2db0bdc9 100644
--- a/app/src/main/res/layout/item_sent_message.xml
+++ b/app/src/main/res/layout/item_sent_message.xml
@@ -23,7 +23,9 @@
android:id="@+id/thread_mesage_attachments_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical" />
+ android:divider="@drawable/linear_layout_vertical_divider"
+ android:orientation="vertical"
+ android:showDividers="middle" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 45e699f0..44c1742c 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -20,7 +20,9 @@
ازالة التثبيت
اعادة ارسال
غير قادر على ضغط الصورة إلى الحجم المحدد
-
+
+ Duplicate item was not included
+
- و %d أخرى
- و %d أخرى
diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml
index 3915a7a8..9aacd6df 100644
--- a/app/src/main/res/values-az/strings.xml
+++ b/app/src/main/res/values-az/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 202d72f2..6100d5e2 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -20,7 +20,9 @@
Адмацаваць
Пераслаць
Немагчыма сціснуць выяву да выбранага памеру
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 2cba551a..23d064f1 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -20,7 +20,9 @@
Откачване
Препращане
Невъзможно е да се компресира изображението до избрания размер
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 687b47a4..5abe55f3 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -20,7 +20,9 @@
No fixis
Reenvia
No s\'ha pogut comprimir la imatge a la mida seleccionada
-
+
+ Duplicate item was not included
+
- i %d altra
- i %d altres
diff --git a/app/src/main/res/values-cr/strings.xml b/app/src/main/res/values-cr/strings.xml
index 3915a7a8..9aacd6df 100644
--- a/app/src/main/res/values-cr/strings.xml
+++ b/app/src/main/res/values-cr/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 2e13b061..8bc00a1b 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -20,7 +20,9 @@
Odepnout
Přeposlat
Nepodařilo se komprimovat obrázek na požadovanou velikost
-
+
+ Duplicate item was not included
+
- a %d další
- a %d další
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 53a255da..7009ba04 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -20,7 +20,9 @@
Frigør
Fremad
Billedet kan ikke komprimeres til den valgte størrelse
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 515cd746..3a62b549 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -20,7 +20,9 @@
Losheften
Weiterleiten
Bild kann nicht auf ausgewählte Größe komprimiert werden
-
+
+ Duplicate item was not included
+
- und %d andere
- und %d anderen
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index f8b7209d..552fe62a 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -20,7 +20,9 @@
Ξεκαρφίτσωμα
Προώθηση
Αδυναμία συμπίεσης εικόνας στο επιλεγμένο μέγεθος
-
+
+ Duplicate item was not included
+
- και άλλος %d
- και άλλοι %d
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index eeacc7a7..68d594ad 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -20,7 +20,9 @@
Depingli
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 6feba91c..ceb257ee 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -20,7 +20,9 @@
Desanclar
Reenviar
Incapaz de comprimir la imagen al tamaño seleccionado
-
+
+ Duplicate item was not included
+
- y %d otro
- y %d otros
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 4d8bbfe3..6dbb1b65 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -20,7 +20,9 @@
Eemalda kinnitus
Edasta
Pildi muutmine valitud suurusesse ei õnnestu
-
+
+ Duplicate item was not included
+
- ja %d muud
- ja %d teised
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 55dc085b..0d9e5527 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -20,7 +20,9 @@
Poista
Välitä
Kuvan pakkaaminen valittuun kokoon ei onnistu
-
+
+ Duplicate item was not included
+
- ja %d muu
- ja %d muuta
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index e04f8cfa..f9b88be2 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -20,7 +20,9 @@
Désépingler
Transférer
Impossible de compresser l\'image à la taille sélectionnée
-
+
+ Duplicate item was not included
+
- et %d autre
- et %d autres
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 6286f752..a87b5d4e 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -20,7 +20,9 @@
Desenganche
Adiante
Non se puido comprimir a imaxe ao tamaño seleccionado
-
+
+ Duplicate item was not included
+
- E %d outro
- E %d outros
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 3915a7a8..9aacd6df 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 8ef1e1e5..6dcdad48 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -20,7 +20,9 @@
Otkvači
Proslijedi
Isključi za komprimiranje slike na odabranu veličinu
-
+
+ Duplicate item was not included
+
- i još %d druga
- i još %d druge
@@ -31,14 +33,14 @@
Dodaj kontakt ili broj …
Prijedlozi
- Scheduled message
- Schedule message
- Schedule send
- Cancel schedule send
- You must pick a time in the future
- Keep the phone on and make sure there is nothing killing the app while in background.
- Update message
- Send now
+ Zakazana poruka
+ Raspored poruka
+ Raspored slanja
+ Otkaži slanje rasporeda
+ Morate odabrati vrijeme u budućnosti
+ Držite telefon uključen i provjerite da ništa ne uništava aplikaciju dok je u pozadini.
+ Ažuriraj poruku
+ Pošalji sada
Primljene SMS poruke
Nova poruka
@@ -85,10 +87,10 @@
Nažalost, potreban je za slanje MMS privitaka. Nemogućnost slanja MMS-a bila bi zaista veliki nedostatak u usporedbi s drugim aplikacijama, pa smo odlučili krenuti ovim putem. No, kao i obično, nema reklama, praćenja ili analiziranja podataka, internet se koristi samo za slanje MMS-a.
Druga strana ne prima moj MMS. Mogu li išta učiniti po tom pitanju\?
Veličinu MMS-a ograničavaju operateri. Možeš pokušati smanjiti ograničenje u postavkama aplikacije.
- Does the app support scheduled messages?
- Yes, you can schedule messages to be sent in the future by long pressing the Send button and picking the desired date and time.
+ Podržava li aplikacija zakazane poruke\?
+ Da, možete zakazati slanje poruka u budućnosti tako da dugo pritisnete gumb Pošalji i odaberete željeni datum i vrijeme.
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index bb51504f..7a47b622 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -20,7 +20,9 @@
Kitűzés megszüntetése
Továbbítás
Nem lehet tömöríteni a képet a kiválasztott méretre
-
+
+ Duplicate item was not included
+
- és még %d fő
- és még %d fő
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 5a18fa37..39efc029 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d others
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 7e95ce1a..84ee0e83 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -20,7 +20,9 @@
Rimuovi
Inoltra
Impossibile comprimere l\'immagine alla dimensione selezionata
-
+
+ Duplicate item was not included
+
- e %d altro
- e %d altri
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 9aab4503..3c9bbe67 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -20,7 +20,9 @@
בטל הצמדה
התקדם
לא ניתן לדחוס תמונה לגודל שנבחר
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 2b58496e..43eb420a 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -20,7 +20,9 @@
固定を外す
転送
選択したサイズに画像を圧縮できません
-
+
+ Duplicate item was not included
+
- and %d others
@@ -80,7 +82,7 @@
メッセージ相手が MMS を受信できません。何かできますか?
MMS のサイズは通信キャリアによって制限されています。アプリの設定で制限を小さくしてみてください。
アプリは予約メッセージをサポートしていますか?
- はい、送信ボタンを長押しして、希望の日付と日時を選択することで、今後送信したいメッセージを予約できます。
+ はい、送信ボタンを長押しして、希望の日付と時間を選択することで、今後送信したいメッセージを予約できます。
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index 42ae7b8b..8bf9fe6f 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 3915a7a8..9aacd6df 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 76ba8c8d..750e431e 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 505a4e94..2d185720 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -20,7 +20,9 @@
Løsne
Videresend
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index dbfdc503..fd0f4c0a 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -20,7 +20,9 @@
Losmaken
Doorsturen
Kon de afbeelding niet comprimeren naar de gekozen grootte
-
+
+ Duplicate item was not included
+
- en %d andere
- en %d anderen
diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml
index 2acff3e8..b566ce26 100644
--- a/app/src/main/res/values-pa-rPK/strings.xml
+++ b/app/src/main/res/values-pa-rPK/strings.xml
@@ -16,11 +16,13 @@
بھیجݨ والا جواب نہیں لیندے اے
کھرڑا
بھیجیا جا رہا اے…
- Pin to the top
- Unpin
+ سکھر تے لگو
+ لگݨ اُلٹاؤ
اگے
تصویر نوں چݨے ہوۓ اکار وچ سنکُچت کر نہیں سکدی
-
+
+ Duplicate item was not included
+
- تے %d ہور
- تے %d ہور
@@ -31,21 +33,21 @@
سُجھے
نیت سنہے
- Schedule message
- Schedule send
- Cancel schedule send
- You must pick a time in the future
+ تالکا وچ سنیہا پایو
+ بھیجݨ دی سمان سارݨی بݨاؤ
+ سماں دا بھیجݨ رد کرو
+ بھوِکھ وچ سماں چݨیو
Keep the phone on and make sure there is nothing killing the app while in background.
- Update message
- Send now
+ سنیہا بدلو
+ ہݨے بھیجو
- Received SMS
- New message
- Mark as Read
- Mark as Unread
- Me
+ سنیہا لیا گیا
+ نواں سنیہا
+ پرھے وجوں چنھت کرو
+ نا پڑھے وجوں چنھت کرو
+ میں
- Are you sure you want to delete all messages of this conversation\?
+ تسیں پکے اے، سارے سنیہے مٹاؤ؟
- %d conversation
@@ -53,35 +55,35 @@
- - %d message
- - %d messages
+ - %d سنیہا
+ - %d سنیہے
Lock screen notification visibility
- Sender and message
- Sender only
+ بھیجݨ والا تے سنیہا
+ صرف بھیجݨ والا
Enable delivery reports
- Remove accents and diacritics at sending messages
+ سنیہے وچ سارے عراب ہٹاؤ
Send message on pressing Enter
Resize sent MMS images
- No limit
+ کوئی حد نہیں
Outgoing messages
Send group messages as MMS
Send long messages as MMS
- Messages
- Export messages
- Export SMS
- Export MMS
- Import messages
- Import SMS
- Import MMS
- You have to select at least one item
+ سنیہے
+ سنیہے ایکسپورٹ کرو
+ سنیہے ایکسپورٹ کرو
+ میڈیا سنیہا ایکسپورٹ کرو
+ سنیہے ایمپورٹ کرو
+ سنیہے ایمپورٹ کرو
+ میڈیا دیاں سنیہے ایمپورٹ کرو
+ کجھ چیز چݨیو
Why does the app require access to the internet\?
Sadly it is needed for sending MMS attachments. Not being able to send MMS would be a really huge disadvantage compared to other apps, so we decided to go this way. However, as usually, there are no ads, tracking or analytics whatsoever, the internet is used only for sending MMS.
The other end is not receiving my MMS, is there anything I can do about it\?
MMS size is limited by carriers, you can try setting a smaller limit in the app settings.
- Does the app support scheduled messages\?
+ ایس اَیپ وچ سنیہاں دا تالکا کیہ اے؟
Yes, you can schedule messages to be sent in the future by long pressing the Send button and picking the desired date and time.
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 1ca4dd55..38e3e17e 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -20,7 +20,9 @@
Odepnij
Przekaż dalej
Nie udało się skompresować obrazu do wybranego rozmiaru
-
+
+ Powielony element nie został uwzględniony
+
- i %d inny
- i %d inne
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 3849d41a..d42ab06b 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -20,7 +20,9 @@
Desfixar
Encaminhar
Não pôde comprimir imagem ao tamanho selecionado
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 96cbdd02..884f28d9 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -20,7 +20,9 @@
Desafixar
Reencaminhar
Incapaz de comprimir imagem no tamanho selecionado
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 0b3cb55f..45561b89 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -20,7 +20,9 @@
Elimină fixarea
Redirecţionare
Nu se poate comprima imaginea la dimensiunea selectată
-
+
+ Duplicate item was not included
+
- și %d alt
- și %d altele
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 8b6cd292..c0ba6ce8 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -20,7 +20,9 @@
Открепить
Переслать
Невозможно сжать изображение до выбранного размера
-
+
+ Duplicate item was not included
+
- и ещё %d
- и ещё %d
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 79813616..cac9b786 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -20,7 +20,9 @@
Odopnúť
Preposlať
Nepodarilo sa zmenšiť obrázok na požadovanú veľkosť
-
+
+ Duplicitná položka nebola pridaná
+
- a %d ďalší
- a %d ďalší
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index cefd7c21..cfdea99f 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -20,7 +20,9 @@
Odpni
Posreduj
Slike ni mogoče stisniti na izbrano velikost
-
+
+ Duplicate item was not included
+
- in %d drug
- in %d druga
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
new file mode 100644
index 00000000..c3c371c6
--- /dev/null
+++ b/app/src/main/res/values-sr/strings.xml
@@ -0,0 +1,91 @@
+
+
+ Једноставан СМС Мессенгер
+ СМС Мессенгер
+ Унесите поруку…
+ Порука није послата
+ Није послата. Додирните да бисте покушали поново.
+ Ваша порука за \'%s\' није послато
+ Додај особу
+ Прилог
+ Није пронађена ниједна сачувана преписка
+ Започните разговор
+ Одговори
+ Прикажи бројач знакова при писању порука
+ Учитавање порука…
+ Пошиљалац не подржава одговоре
+ Нацрт
+ Шаље се…
+ Закачите на врх
+ Oткопчати
+ Напред
+ Није могуће компримовати слику на изабрану величину
+
+ Duplicate item was not included
+
+
+ - и %d друго
+ - и %d других
+ - и %d други
+
+
+ Нови разговор
+ Додајте контакт или број…
+ Предлози
+
+ Заказана порука
+ Закажите поруку
+ Закажите слање
+ Откажи слање распореда
+ Морате изабрати време у будућности
+ Држите телефон укључен и уверите се да ништа не убија апликацију док је у позадини.
+ Ажурирајте поруку
+ Пошаљи одмах
+
+ Примите СМС
+ Нова порука
+ Означи као прочитано
+ Означи као непрочитанo
+ Ja
+
+ Да ли сте сигурни да желите да избришете све поруке ове конверзације\?
+
+
+ - %d разговор
+ - %d разговорa
+ - %d разговори
+
+
+
+ - %d порука
+ - %d порука
+ - %d порукe
+
+
+ Видљивост обавештења на закључаном екрану
+ Пошиљалац и порука
+ Само пошиљалац
+ Омогућите извештаје о испоруци
+ Уклоните акценте и дијакритичке знакове при слању порука
+ Промените величину посланих ММС слика
+ Без лимита
+ Одлазне поруке
+ Пошаљите групне поруке као ММС
+ Шаљите дугачке поруке као ММС
+
+ Поруке
+ Извезите поруке
+ Извези СМС
+ Извези ММС
+ Увезите поруке
+ Увезите СМС
+ Увези ММС
+ Морате да изаберете најмање једну ставку
+
+ Зашто је апликацији потребан приступ интернету\?
+ Нажалост, то је потребно за слање ММС прилога. Немогућност слања ММС-а била би заиста велика мана у поређењу са другим апликацијама, па смо одлучили да идемо овим путем. Међутим, као и обично, нема реклама, праћења или аналитике, интернет се користи само за слање ММС-а.
+ Други крај не прима мој ММС, да ли могу нешто да урадим поводом тога\?
+ Величина ММС-а је ограничена оператерима, можете покушати да подесите мање ограничење у подешавањима апликације.
+ Да ли апликација подржава заказане поруке\?
+ Да, можете заказати слање порука у будућности дугим притиском на дугме Пошаљи и одабиром жељеног датума и времена.
+
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index a6375a86..d13e8278 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -20,7 +20,9 @@
Lossa
Vidarebefordra
Det gick inte att komprimera bilden till den valda storleken
-
+
+ Duplicate item was not included
+
- och %d annan
- och %d andra
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 31e55bde..b93e7db6 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -20,7 +20,9 @@
பின் நீக்கு
முன்னோக்கி
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index fa998dff..eca205a0 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d others
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 17537de4..d6d2e020 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -20,7 +20,9 @@
Sabitlemeyi kaldır
İlet
Resim seçilen boyuta sıkıştırılamıyor
-
+
+ Duplicate item was not included
+
- ve %d diğeri
- ve %d diğeri
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 560a34fd..42b76005 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -20,7 +20,9 @@
Відкріпити
Переслати
Не вдається стиснути зображення до вибраного розміру
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a4a1ecc6..ac3365cc 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -20,9 +20,11 @@
取消固定
转发
无法将图像压缩到选定的大小
-
+
+ 未包含重复项
+
- - 及 %d 个其他人
+ - 及其他 %d 个联系人
新的对话
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index c43962a4..6c16004a 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -20,7 +20,9 @@
取消釘選
轉傳
無法將圖片壓縮至指定大小
-
+
+ Duplicate item was not included
+
- and %d others
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e0959a28..324712d5 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -3,9 +3,15 @@
72dp
64dp
36dp
- 60dp
+ 64dp
+ @dimen/attachment_preview_size
+ 156dp
24dp
15dp
64dp
20dp
+ 36dp
+ 96dp
+ 90dp
+ 250dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 62a2740c..11360f37 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,7 +20,9 @@
Unpin
Forward
Unable to compress image to selected size
-
+
+ Duplicate item was not included
+
- and %d other
- and %d others
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index 758c02cd..7c909b1b 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt
index ae4bcf40..88fa685b 100644
--- a/fastlane/metadata/android/de-DE/short_description.txt
+++ b/fastlane/metadata/android/de-DE/short_description.txt
@@ -1 +1 @@
-SMS- und MMS-Nachrichten schnell versenden. Saubere schöne Schnittstelle
+SMS- und MMS-Nachrichten schnell versenden, schöne Benutzeroberfläche
diff --git a/fastlane/metadata/android/de-DE/video.txt b/fastlane/metadata/android/de-DE/video.txt
new file mode 100644
index 00000000..8d1c8b69
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/video.txt
@@ -0,0 +1 @@
+
diff --git a/fastlane/metadata/android/es-ES/title.txt b/fastlane/metadata/android/es-ES/title.txt
index 627c2daa..f572513d 100644
--- a/fastlane/metadata/android/es-ES/title.txt
+++ b/fastlane/metadata/android/es-ES/title.txt
@@ -1 +1 @@
-Sencilla Mensajería de SMS
+Mensajería Sencilla de SMS
diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt
new file mode 100644
index 00000000..1088d31d
--- /dev/null
+++ b/fastlane/metadata/android/hr/full_description.txt
@@ -0,0 +1,27 @@
+Sjajan način da ostanete u kontaktu sa rodbinom, slanjem poruka. Aplikacija ispravno obrađuje i grupne poruke, kao i blokiranje brojeva s Androida 7+. Ostanite u kontaktu sa svojim kontaktima pomoću aplikacije za razmjenu poruka na telefonu. Nikada nije bilo lakše dijeliti fotografije, slati emojije ili samo pozdraviti. Postoji toliko toga što možete učiniti sa porukama, poput isključivanja zvuka razgovora ili dodjele posebnih tonova poruka za određene kontakte. S aplikacijom za poruke možete uživati u svakodnevnoj razmjeni poruka na zabavniji način.
+
+Nudi mnoge formate datuma koje možete izabrati kako biste se osjećali ugodno dok ga koristite. Također možete mijenjati format vremena između 12 i 24 sata. Ova vam aplikacija također daje fleksibilnost sigurnosnog kopiranja SMS-a. Ne morate spremati poruke na bilo koji vanjski uređaj ili koristiti bilo koji drugi hardver za njihovo spremanje. Ova značajka sigurnosne kopije SMS-a pomoći će vam da spremite tekstualne poruke i mms podatke bez interne pohrane.
+
+Ova aplikacija za razmjenu poruka ima stvarno malu veličinu aplikacije u usporedbi s konkurencijom, što ju čini vrlo brzom za preuzimanje. Tehnika sigurnosne kopije sms-a korisna je kada morate promijeniti svoj uređaj ili vam ga ukradu. Na ovaj način možete lako dohvatiti tekstualnu poruku i iz grupne i iz privatne razmjene poruka pomoću sigurnosne kopije sms-a u ovoj aplikaciji za razmjenu.
+
+Značajka blokiranja pomaže u sprječavanju neželjenih poruka, možete blokirati i poruke kontakata. Blokirani brojevi mogu se izvoziti i uvoziti za kopiranje. Svi razgovori mogu se jednostavno izvesti u datoteku za sigurnosno kopiranje ili migraciju između uređaja.
+
+Možete prilagoditi koji će dio poruke biti vidljiv na zaslonu. Možete odabrati želite li da se prikazuje samo pošiljatelj, poruka ili ništa za privatnost.
+
+Aplikacija za razmjenu poruka korisnicima također pruža mogućnost brzog i učinkovitog pretraživanja poruka. Prošli su dani kada se morate pomicati kroz sve privatne i grupne razgovore kako biste došli do željene poruke. Pretražite i dobijte što želite s ovom aplikacijom za razmjenu poruka.
+
+Dolazi s materijalnim dizajnom i tamnom temom prema zadanim postavkama, pruža izvrsno korisničko iskustvo za jednostavno korištenje. Nedostatak pristupa internetu daje vam više privatnosti, sigurnosti i stabilnosti od aplikacija.
+
+Ne sadrži oglase niti dopuštenja. Potpuno je otvorenog koda, pruža boje.
+
+Ovdje pogledajte kompletan paket jednostavnih alata:
+https://www.simplemobiletools.com
+
+Facebook:
+https://www.facebook.com/simplemobiletools
+
+Reddit:
+https://www.reddit.com/r/SimpleMobileTools
+
+Telegram:
+https://t.me/SimpleMobileTools
diff --git a/fastlane/metadata/android/hr/short_description.txt b/fastlane/metadata/android/hr/short_description.txt
new file mode 100644
index 00000000..42571d90
--- /dev/null
+++ b/fastlane/metadata/android/hr/short_description.txt
@@ -0,0 +1 @@
+Aplikacija za razmjenu poruka za brzo slanje poruka, lijepo korisničko sučelje
diff --git a/fastlane/metadata/android/hr/title.txt b/fastlane/metadata/android/hr/title.txt
new file mode 100644
index 00000000..cca56e43
--- /dev/null
+++ b/fastlane/metadata/android/hr/title.txt
@@ -0,0 +1 @@
+Jednostavan SMS slatelj
diff --git a/fastlane/metadata/android/pa-PK/short_description.txt b/fastlane/metadata/android/pa-PK/short_description.txt
new file mode 100644
index 00000000..f7aad2be
--- /dev/null
+++ b/fastlane/metadata/android/pa-PK/short_description.txt
@@ -0,0 +1 @@
+آنڈروئیڈ لئی سنیہے دی ایپلیکیشن، جالدی بھیجدے، اِنٹرفیس چنگی اے
diff --git a/fastlane/metadata/android/pa-PK/title.txt b/fastlane/metadata/android/pa-PK/title.txt
new file mode 100644
index 00000000..30b94791
--- /dev/null
+++ b/fastlane/metadata/android/pa-PK/title.txt
@@ -0,0 +1 @@
+سادے سنہے
diff --git a/gradle.properties b/gradle.properties
index 23339e0d..dbb7bf70 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,21 +1,2 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
-# Kotlin code style for this project: "official" or "obsolete":
-kotlin.code.style=official
+android.useAndroidX=true