Add vCard attachment preview

This commit is contained in:
Naveen 2022-08-29 03:19:50 +05:30
parent d874e16024
commit f07abeb54c
58 changed files with 1263 additions and 117 deletions

View file

@ -0,0 +1,81 @@
package com.simplemobiletools.smsmessenger.activities
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.simplemobiletools.commons.extensions.normalizePhoneNumber
import com.simplemobiletools.commons.helpers.NavigationIcon
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.VCardViewerAdapter
import com.simplemobiletools.smsmessenger.extensions.dialNumber
import com.simplemobiletools.smsmessenger.extensions.sendMail
import com.simplemobiletools.smsmessenger.helpers.EXTRA_VCARD_URI
import com.simplemobiletools.smsmessenger.helpers.parseVCardFromUri
import com.simplemobiletools.smsmessenger.models.VCardPropertyWrapper
import com.simplemobiletools.smsmessenger.models.VCardWrapper
import ezvcard.VCard
import ezvcard.property.Email
import ezvcard.property.Telephone
import kotlinx.android.synthetic.main.activity_vcard_viewer.*
class VCardViewerActivity : SimpleActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vcard_viewer)
val vCardUri = intent.getParcelableExtra(EXTRA_VCARD_URI) as? Uri
if (vCardUri != null) {
setupOptionsMenu(vCardUri)
parseVCardFromUri(this, vCardUri) {
runOnUiThread {
setupContactsList(it)
}
}
}
}
override fun onResume() {
super.onResume()
setupToolbar(vcard_toolbar, NavigationIcon.Arrow)
}
private fun setupOptionsMenu(vCardUri: Uri) {
vcard_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.add_contact -> {
val intent = Intent(Intent.ACTION_VIEW).apply {
val mimetype = contentResolver.getType(vCardUri)
setDataAndType(vCardUri, mimetype?.lowercase())
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
}
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun setupContactsList(vCards: List<VCard>) {
val items = prepareData(vCards)
val adapter = VCardViewerAdapter(this, items.toMutableList()) { item ->
val property = item as? VCardPropertyWrapper
if (property != null) {
handleClick(item)
}
}
contacts_list.adapter = adapter
}
private fun handleClick(property: VCardPropertyWrapper) {
when (property.property) {
is Telephone -> dialNumber(property.value.normalizePhoneNumber())
is Email -> sendMail(property.value)
}
}
private fun prepareData(vCards: List<VCard>): List<VCardWrapper> {
return vCards.map { VCardWrapper(it) }
}
}

View file

@ -32,13 +32,13 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.NewConversationActivity
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.activities.ThreadActivity
import com.simplemobiletools.smsmessenger.activities.VCardViewerActivity
import com.simplemobiletools.smsmessenger.dialogs.SelectTextDialog
import com.simplemobiletools.smsmessenger.extensions.deleteMessage
import com.simplemobiletools.smsmessenger.extensions.getContactFromAddress
import com.simplemobiletools.smsmessenger.extensions.updateLastConversationMessage
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_unknown_attachment.view.*
import kotlinx.android.synthetic.main.item_sent_unknown_attachment.view.*
@ -290,102 +290,167 @@ class ThreadAdapter(
if (message.attachment?.attachments?.isNotEmpty() == true) {
for (attachment in message.attachment.attachments) {
val mimetype = attachment.mimetype
val uri = attachment.getUri()
if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
val imageView = layoutInflater.inflate(R.layout.item_attachment_image, null)
thread_mesage_attachments_holder.addView(imageView)
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)
}
val placeholderDrawable = ColorDrawable(Color.TRANSPARENT)
val isTallImage = attachment.height > attachment.width
val transformation = if (isTallImage) CenterCrop() else FitCenter()
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(placeholderDrawable)
.transform(transformation)
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
}
}
}
}
var builder = Glide.with(context)
.load(uri)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(options)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
thread_message_play_outline.beGone()
thread_mesage_attachments_holder.removeView(imageView)
return false
}
private fun setupImageView(holder: ViewHolder, parent: View, message: Message, attachment: Attachment) {
val mimetype = attachment.mimetype
val uri = attachment.getUri()
parent.apply {
val imageView = layoutInflater.inflate(R.layout.item_attachment_image, null)
thread_mesage_attachments_holder.addView(imageView)
override fun onResourceReady(dr: Drawable?, a: Any?, t: Target<Drawable>?, d: DataSource?, i: Boolean) =
false
})
val placeholderDrawable = ColorDrawable(Color.TRANSPARENT)
val isTallImage = attachment.height > attachment.width
val transformation = if (isTallImage) CenterCrop() else FitCenter()
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(placeholderDrawable)
.transform(transformation)
builder = if (isTallImage) {
builder.override(attachment.width, attachment.width)
var builder = Glide.with(context)
.load(uri)
.transition(DrawableTransitionOptions.withCrossFade())
.apply(options)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
thread_message_play_outline.beGone()
thread_mesage_attachments_holder.removeView(imageView)
return false
}
override fun onResourceReady(dr: Drawable?, a: Any?, t: Target<Drawable>?, d: DataSource?, i: Boolean) =
false
})
builder = if (isTallImage) {
builder.override(attachment.width, attachment.width)
} else {
builder.override(attachment.width, attachment.height)
}
builder.into(imageView.attachment_image)
imageView.attachment_image.setOnClickListener {
if (actModeCallback.isSelectable) {
holder.viewClicked(message)
} else {
launchViewIntent(uri, mimetype, attachment.filename)
}
}
imageView.setOnLongClickListener {
holder.viewLongClicked()
true
}
}
}
private fun setupVCardView(holder: ViewHolder, parent: View, message: Message, attachment: Attachment) {
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)
vcard_view_contact.setTextColor(context.getLinkTextColor())
}
thread_mesage_attachments_holder.addView(vCardView)
parseVCardFromUri(context, uri) { vCards ->
val title = vCards.first().formattedName.value
val imageIcon = SimpleContactsHelper(context).getContactLetterIcon(title)
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 {
builder.override(attachment.width, attachment.height)
vcard_subtitle.beGone()
}
vcard_view_contact.text = resources.getQuantityString(R.plurals.view_contact, vCards.size)
builder.into(imageView.attachment_image)
imageView.attachment_image.setOnClickListener {
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
}
}
}
}
}
}
private fun setupFileView(holder: ViewHolder, parent: View, message: Message, attachment: Attachment) {
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
}
setTextColor(textColor)
setOnClickListener {
if (actModeCallback.isSelectable) {
holder.viewClicked(message)
} else {
launchViewIntent(uri, mimetype, attachment.filename)
}
}
imageView.setOnLongClickListener {
setOnLongClickListener {
holder.viewLongClicked()
true
}
} else {
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
}
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)
}
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)
}
setOnLongClickListener {
holder.viewLongClicked()
true
}
}
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
}
thread_mesage_attachments_holder.addView(attachmentView)
}
}
}

View file

@ -0,0 +1,138 @@
package com.simplemobiletools.smsmessenger.adapters
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.extensions.getTextSize
import com.simplemobiletools.commons.extensions.onGlobalLayout
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.models.VCardPropertyWrapper
import com.simplemobiletools.smsmessenger.models.VCardWrapper
import kotlinx.android.synthetic.main.item_vcard_contact.view.*
import kotlinx.android.synthetic.main.item_vcard_contact_property.view.*
class VCardViewerAdapter(
private val activity: SimpleActivity, private var items: MutableList<Any>, private val itemClick: (Any) -> Unit
) : RecyclerView.Adapter<VCardViewerAdapter.VCardViewHolder>() {
private var fontSize = activity.getTextSize()
private var textColor = activity.getProperTextColor()
private val layoutInflater = activity.layoutInflater
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return when (val item = items[position]) {
is VCardWrapper -> R.layout.item_vcard_contact
is VCardPropertyWrapper -> R.layout.item_vcard_contact_property
else -> throw IllegalArgumentException("Unexpected type: ${item::class.simpleName}")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VCardViewHolder {
val view = layoutInflater.inflate(viewType, parent, false)
return VCardViewHolder(view)
}
override fun onBindViewHolder(holder: VCardViewerAdapter.VCardViewHolder, position: Int) {
val item = items[position]
val itemView = holder.bindView()
when (item) {
is VCardWrapper -> setupVCardView(itemView, item)
is VCardPropertyWrapper -> setupVCardPropertyView(itemView, item)
else -> throw IllegalArgumentException("Unexpected type: ${item::class.simpleName}")
}
}
private fun setupVCardView(view: View, item: VCardWrapper) {
val name = item.vCard.formattedName.value
view.apply {
item_contact_name.apply {
text = name
setTextColor(textColor)
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
}
item_contact_image.apply {
val photo = item.vCard.photos.firstOrNull()
val placeholder = SimpleContactsHelper(context).getContactLetterIcon(name).toDrawable(resources)
val roundingRadius = resources.getDimensionPixelSize(R.dimen.big_margin)
val transformation = RoundedCorners(roundingRadius)
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(placeholder)
.transform(transformation)
Glide.with(this)
.load(photo?.data ?: photo?.url)
.apply(options)
.transition(DrawableTransitionOptions.withCrossFade())
.into(this)
}
setOnClickListener {
expandOrCollapseRow(view, item)
}
onGlobalLayout {
if (items.size == 1) {
expandOrCollapseRow(view, item)
}
}
}
}
private fun setupVCardPropertyView(view: View, property: VCardPropertyWrapper) {
view.apply {
item_vcard_property_title.apply {
text = property.value
setTextColor(textColor)
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f)
}
item_vcard_property_subtitle.apply {
text = property.type
setTextColor(textColor)
}
view.setOnClickListener {
itemClick(property)
}
}
}
private fun expandOrCollapseRow(view: View, item: VCardWrapper) {
val properties = item.getVCardProperties(context = activity)
if (item.expanded) {
collapseRow(view, properties, item)
} else {
expandRow(view, properties, item)
}
}
private fun expandRow(view: View, properties: List<VCardPropertyWrapper>, vCardWrapper: VCardWrapper) {
vCardWrapper.expanded = true
val nextPosition = items.indexOf(vCardWrapper) + 1
items.addAll(nextPosition, properties)
notifyItemRangeInserted(nextPosition, properties.size)
view.expand_collapse_icon.setImageResource(R.drawable.ic_collapse_up)
}
private fun collapseRow(view: View, properties: List<VCardPropertyWrapper>, vCardWrapper: VCardWrapper) {
vCardWrapper.expanded = false
val nextPosition = items.indexOf(vCardWrapper) + 1
repeat(properties.size) {
items.removeAt(nextPosition)
}
notifyItemRangeRemoved(nextPosition, properties.size)
view.expand_collapse_icon.setImageResource(R.drawable.ic_expand_down)
}
inner class VCardViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView() = itemView
}
}

View file

@ -24,3 +24,19 @@ fun Activity.dialNumber(phoneNumber: String, callback: (() -> Unit)? = null) {
}
}
}
fun Activity.sendMail(email: String) {
hideKeyboard()
Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, email)
try {
startActivity(this)
} catch (e: ActivityNotFoundException) {
toast(R.string.no_app_found)
} catch (e: Exception) {
showErrorToast(e)
}
}
}

View file

@ -0,0 +1,8 @@
package com.simplemobiletools.smsmessenger.extensions
import android.text.format.DateFormat
import java.util.*
fun Date.format(pattern: String): String {
return DateFormat.format(pattern, this).toString()
}

View file

@ -14,3 +14,8 @@ fun String.getExtensionFromMimeType(): String {
fun String.isImageMimeType(): Boolean {
return lowercase().startsWith("image")
}
fun String.isVCardMimeType(): Boolean {
val lowercase = lowercase()
return lowercase.endsWith("x-vcard") || lowercase.endsWith("vcard")
}

View file

@ -28,6 +28,7 @@ const val EXPORT_FILE_EXT = ".json"
const val IMPORT_SMS = "import_sms"
const val IMPORT_MMS = "import_mms"
const val WAS_DB_CLEARED = "was_db_cleared_2"
const val EXTRA_VCARD_URI = "vcard"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"

View file

@ -0,0 +1,15 @@
package com.simplemobiletools.smsmessenger.helpers
import android.content.Context
import android.net.Uri
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import ezvcard.Ezvcard
import ezvcard.VCard
fun parseVCardFromUri(context: Context, uri: Uri, callback: (vCards: List<VCard>) -> Unit) {
ensureBackgroundThread {
val inputStream = context.contentResolver.openInputStream(uri)
val vCards = Ezvcard.parse(inputStream).all()
callback(vCards)
}
}

View file

@ -0,0 +1,54 @@
package com.simplemobiletools.smsmessenger.models
import android.content.Context
import com.simplemobiletools.commons.extensions.normalizePhoneNumber
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.format
import ezvcard.VCard
import ezvcard.property.*
private val displayedPropertyClasses = arrayOf(
Telephone::class.java, Email::class.java, Organization::class.java, Birthday::class.java, Anniversary::class.java, Note::class.java
)
data class VCardWrapper(val vCard: VCard, var expanded: Boolean = false) {
fun getVCardProperties(context: Context): List<VCardPropertyWrapper> {
return vCard.properties
.filter { displayedPropertyClasses.contains(it::class.java) }
.map { VCardPropertyWrapper.from(context, it) }
}
}
data class VCardPropertyWrapper(val value: String, val type: String, val property: VCardProperty) {
companion object {
private const val CELL = "CELL"
private const val HOME = "HOME"
private const val WORK = "WORK"
private fun VCardProperty.getPropertyTypeString(context: Context): String {
return when (parameters.type) {
CELL -> context.getString(R.string.mobile)
HOME -> context.getString(R.string.home)
WORK -> context.getString(R.string.work)
else -> ""
}
}
fun from(context: Context, property: VCardProperty): VCardPropertyWrapper {
return property.run {
when (this) {
is Telephone -> VCardPropertyWrapper(text.normalizePhoneNumber(), getPropertyTypeString(context), property)
is Email -> VCardPropertyWrapper(value, getPropertyTypeString(context), property)
is Organization -> VCardPropertyWrapper(values.joinToString(), context.getString(R.string.work), property)
is Birthday -> VCardPropertyWrapper(date.format(context.config.dateFormat), context.getString(R.string.birthday), property)
is Anniversary -> VCardPropertyWrapper(date.format(context.config.dateFormat), context.getString(R.string.anniversary), property)
is Note -> VCardPropertyWrapper(value, context.getString(R.string.notes), property)
else -> VCardPropertyWrapper("", "", property)
}
}
}
}
}