diff --git a/app/build.gradle b/app/build.gradle index f208ea6e..7fd7b75b 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:d5ae570e2a' 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 abe5a432..0e088d86 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -5,7 +5,6 @@ import android.app.Activity import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.BitmapFactory -import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.media.MediaMetadataRetriever import android.net.Uri @@ -27,21 +26,15 @@ import android.util.TypedValue import android.view.Gravity import android.view.View import android.view.WindowManager +import android.view.animation.OvershootInterpolator import android.view.inputmethod.EditorInfo import android.widget.LinearLayout import android.widget.LinearLayout.LayoutParams import android.widget.RelativeLayout +import androidx.annotation.StringRes +import androidx.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 @@ -53,6 +46,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 @@ -60,8 +54,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 @@ -72,12 +66,6 @@ import java.io.OutputStream class ThreadActivity : SimpleActivity() { private val MIN_DATE_TIME_DIFF_SECS = 300 - private val PICK_ATTACHMENT_INTENT = 1 - private val PICK_SAVE_FILE_INTENT = 11 - private val TAKE_PHOTO_INTENT = 42 - - private val TYPE_TAKE_PHOTO = 12 - private val TYPE_CHOOSE_PHOTO = 13 private val TYPE_EDIT = 14 private val TYPE_SEND = 15 @@ -93,8 +81,6 @@ class ThreadActivity : SimpleActivity() { private var privateContacts = ArrayList() private var messages = ArrayList() private val availableSIMCards = ArrayList() - private var attachmentSelections = mutableMapOf() - private val imageCompressor by lazy { ImageCompressor(this) } private var lastAttachmentUri: String? = null private var capturedImageUri: Uri? = null private var loadingOlderMessages = false @@ -105,6 +91,8 @@ class ThreadActivity : SimpleActivity() { private var scheduledMessage: Message? = null private lateinit var scheduledDateTime: DateTime + private var isAttachmentPickerVisible = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_thread) @@ -145,6 +133,10 @@ class ThreadActivity : SimpleActivity() { finish() } } + + setupAttachmentPickerView() + setupKeyboardListener() + hideAttachmentPicker() } override fun onResume() { @@ -161,17 +153,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) @@ -214,13 +214,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) + } } } @@ -371,11 +374,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) + } } } @@ -427,6 +432,7 @@ class ThreadActivity : SimpleActivity() { setTextColor(textColor) compoundDrawables.forEach { it?.applyColorFilter(textColor) } } + confirm_manage_contacts.applyColorFilter(textColor) thread_add_attachment.applyColorFilter(textColor) @@ -440,6 +446,7 @@ class ThreadActivity : SimpleActivity() { thread_send_message.setOnClickListener { sendMessage() } + thread_send_message.setOnLongClickListener { if (!isScheduledMessage) { launchScheduleSendDialog() @@ -478,7 +485,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) { @@ -775,135 +790,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() } @@ -928,7 +938,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 @@ -942,7 +952,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 } @@ -982,13 +992,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)) @@ -997,8 +1010,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 @@ -1017,9 +1029,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 @@ -1144,14 +1155,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) } } @@ -1199,7 +1203,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() { @@ -1212,15 +1216,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)), @@ -1277,7 +1272,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) @@ -1344,7 +1339,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, @@ -1352,11 +1347,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 066cbf13..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,11 @@ package com.simplemobiletools.smsmessenger.adapters import android.annotation.SuppressLint -import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable -import android.net.Uri import android.util.Size import android.util.TypedValue import android.view.Menu @@ -39,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 @@ -288,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/")) @@ -401,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 { @@ -415,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 - } - } - } - } } } @@ -466,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 57c5ad8d..c1fa3d74 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) { @@ -86,4 +87,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 15baf492..bf8f2761 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -30,6 +30,7 @@ const val IMPORT_MMS = "import_mms" const val WAS_DB_CLEARED = "was_db_cleared_2" const val EXTRA_VCARD_URI = "vcard" const val SCHEDULED_MESSAGE_ID = "scheduled_message_id" +const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height" private const val PATH = "com.simplemobiletools.smsmessenger.action." const val MARK_AS_READ = PATH + "mark_as_read" @@ -45,6 +46,11 @@ const val THREAD_SENT_MESSAGE_ERROR = 4 const val THREAD_SENT_MESSAGE_SENT = 5 const val THREAD_SENT_MESSAGE_SENDING = 6 +// view types for attachment list +const val ATTACHMENT_DOCUMENT = 7 +const val ATTACHMENT_MEDIA = 8 +const val ATTACHMENT_VCARD = 9 + // lock screen visibility constants const val LOCK_SCREEN_SENDER_MESSAGE = 1 const val LOCK_SCREEN_SENDER = 2 @@ -60,6 +66,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 bbda648f..dbcf6559 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 أخرى @@ -99,4 +101,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index ce4cfab9..d3ecbea8 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 3cb94d5c..ae2b2073 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 8a4ba571..1f708488 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 5818bef5..672e8f60 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-cr/strings.xml b/app/src/main/res/values-cr/strings.xml index ce4cfab9..d3ecbea8 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 826c1fc6..8eafa162 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 e7eee89a..a56e24ab 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 2070b2f8..6983cc16 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index b736ea37..84e8d44c 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 8efb4fed..bfd97ea1 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 14149d72..95c07450 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 @@ -90,4 +92,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0d38e20e..19dccea1 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 9002ea6c..57764854 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 0151d724..a6362373 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 @@ -90,4 +92,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 8d0b9a2d..9be31218 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 @@ -87,4 +89,4 @@ Non atopaches algunhas cadeas? Hai máis en https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index ce4cfab9..d3ecbea8 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 14cd1f2c..3fda8528 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 diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c5cc383b..8e99341d 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 e4307a8b..f09f240e 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 5d50ef49..818c4175 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 @@ -90,4 +92,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d1abdaa8..578270c1 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 204b05cf..c33d2132 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 @@ -84,4 +86,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 2f6a4218..fc00464f 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -20,7 +20,9 @@ Atjunkite Pirmyn Nepavyksta suspausti vaizdo iki pasirinkto dydžio - + + 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 b49e6d50..3f955f9e 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 ce4cfab9..d3ecbea8 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 284ab728..7e4bb7fe 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 3a6750d7..fd7d141f 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 fca96f51..552320b0 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index a2d002f7..66f5a68c 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -20,7 +20,9 @@ Unpin اگے تصویر نوں چݨے ہوۓ اکار وچ سنکُچت کر نہیں سکدی - + + Duplicate item was not included + تے %d ہور تے %d ہور diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0d013873..50cbb1d9 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 - + + Duplicate item was not included + i %d inny i %d inne @@ -93,4 +95,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fce03488..e9adec12 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 66339642..7a4e6f27 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 6fb541ce..8883889b 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 3ad117cc..05ca7ed8 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 @@ -93,4 +95,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 28cd4030..d1fe3311 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ť - + + Duplicate item was not included + 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 388241a0..8a14192f 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 @@ -93,4 +95,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c9fe0303..01abcdaa 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 65cbce32..c5f0f542 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 aca6f73e..e5eaf577 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 c03eb152..e778a209 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 @@ -87,4 +89,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ac09c46c..70c414ca 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 1f50f0b6..d39fd9c8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -20,7 +20,9 @@ 取消固定 转发 无法将图像压缩到选定的大小 - + + Duplicate item was not included + 及 %d 个其他人 @@ -84,4 +86,4 @@ Haven't found some strings? There's more at https://github.com/SimpleMobileTools/Simple-Commons/tree/master/commons/src/main/res --> - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e553273c..661c5b8a 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 d05f0dc0..ed8a94d4 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 @@ - +