Added contact export as vCard from contacts list

This commit is contained in:
Sylvain Berfini 2023-08-27 12:10:32 +02:00
parent 855b03fb34
commit 5af3a490d4
6 changed files with 99 additions and 14 deletions

View file

@ -93,6 +93,7 @@ class ContactFragment : GenericFragment() {
} }
binding.setShareClickListener { binding.setShareClickListener {
Log.i("$TAG Sharing friend, exporting it as vCard file first")
viewModel.exportContactAsVCard() viewModel.exportContactAsVCard()
} }
@ -173,9 +174,13 @@ class ContactFragment : GenericFragment() {
} }
viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) { viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) {
it.consume { file -> it.consume { pair ->
Log.i("$TAG Friend was exported as vCard file [${file.absolutePath}]") val contactName = pair.first
shareContact(file) val file = pair.second
Log.i(
"$TAG Friend [$contactName] was exported as vCard file [${file.absolutePath}], sharing it"
)
shareContact(contactName, file)
} }
} }
@ -203,7 +208,7 @@ class ContactFragment : GenericFragment() {
) )
} }
private fun shareContact(file: File) { private fun shareContact(name: String, file: File) {
val publicUri = FileProvider.getUriForFile( val publicUri = FileProvider.getUriForFile(
requireContext(), requireContext(),
requireContext().getString(R.string.file_provider), requireContext().getString(R.string.file_provider),
@ -214,7 +219,7 @@ class ContactFragment : GenericFragment() {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri) putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, viewModel.contact.value?.friend?.name) putExtra(Intent.EXTRA_SUBJECT, name)
type = ContactsContract.Contacts.CONTENT_VCARD_TYPE type = ContactsContract.Contacts.CONTENT_VCARD_TYPE
} }

View file

@ -19,17 +19,21 @@
*/ */
package org.linphone.ui.main.contacts.fragment package org.linphone.ui.main.contacts.fragment
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.ContactsContract
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import java.io.File
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
@ -115,6 +119,17 @@ class ContactsListFragment : GenericFragment() {
Log.i("$TAG Favourites contacts list is ready with [${it.size}] items") Log.i("$TAG Favourites contacts list is ready with [${it.size}] items")
} }
listViewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) {
it.consume { pair ->
val contactName = pair.first
val file = pair.second
Log.i(
"$TAG Friend [$contactName] was exported as vCard file [${file.absolutePath}], sharing it"
)
shareContact(contactName, file)
}
}
sharedViewModel.searchFilter.observe(viewLifecycleOwner) { sharedViewModel.searchFilter.observe(viewLifecycleOwner) {
it.consume { filter -> it.consume { filter ->
listViewModel.applyFilter(filter) listViewModel.applyFilter(filter)
@ -130,7 +145,7 @@ class ContactsListFragment : GenericFragment() {
adapter.contactLongClickedEvent.observe(viewLifecycleOwner) { adapter.contactLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model -> it.consume { model ->
val modalBottomSheet = ContactsListMenuDialogFragment( val modalBottomSheet = ContactsListMenuDialogFragment(
model.friend.starred, model.starred,
{ // onDismiss { // onDismiss
adapter.resetSelection() adapter.resetSelection()
}, },
@ -147,8 +162,10 @@ class ContactsListFragment : GenericFragment() {
} }
}, },
{ // onShare { // onShare
Log.i("$TAG Sharing friend [${model.name.value}]") Log.i(
// TODO "$TAG Sharing friend [${model.name.value}], exporting it as vCard file first"
)
listViewModel.exportContactAsVCard(model.friend)
}, },
{ // onDelete { // onDelete
coreContext.postOnCoreThread { coreContext.postOnCoreThread {
@ -168,4 +185,23 @@ class ContactsListFragment : GenericFragment() {
} }
} }
} }
private fun shareContact(name: String, file: File) {
val publicUri = FileProvider.getUriForFile(
requireContext(),
requireContext().getString(R.string.file_provider),
file
)
Log.i("$TAG Public URI for vCard file is [$publicUri], starting intent chooser")
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, publicUri)
putExtra(Intent.EXTRA_SUBJECT, name)
type = ContactsContract.Contacts.CONTENT_VCARD_TYPE
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
} }

View file

@ -37,6 +37,8 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) {
val id = friend.refKey val id = friend.refKey
val starred = friend.starred
val avatar = MutableLiveData<Uri>() val avatar = MutableLiveData<Uri>()
val initials = LinphoneUtils.getInitials(friend.name.orEmpty()) val initials = LinphoneUtils.getInitials(friend.name.orEmpty())

View file

@ -80,8 +80,8 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<String>>() MutableLiveData<Event<String>>()
} }
val vCardTerminatedEvent: MutableLiveData<Event<File>> by lazy { val vCardTerminatedEvent: MutableLiveData<Event<Pair<String, File>>> by lazy {
MutableLiveData<Event<File>>() MutableLiveData<Event<Pair<String, File>>>()
} }
val displayTrustProcessDialogEvent: MutableLiveData<Event<Boolean>> by lazy { val displayTrustProcessDialogEvent: MutableLiveData<Event<Boolean>> by lazy {
@ -287,11 +287,14 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase( val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase(
Locale.getDefault() Locale.getDefault()
) )
val file = FileUtils.getFileStorageCacheDir("$fileName.vcf") val file = FileUtils.getFileStorageCacheDir(
"$fileName.vcf",
overrideExisting = true
)
viewModelScope.launch { viewModelScope.launch {
if (FileUtils.dumpStringToFile(vCard, file)) { if (FileUtils.dumpStringToFile(vCard, file)) {
Log.i("$TAG vCard string saved as file in cache folder") Log.i("$TAG vCard string saved as file in cache folder")
vCardTerminatedEvent.postValue(Event(file)) vCardTerminatedEvent.postValue(Event(Pair(friend.name.orEmpty(), file)))
} else { } else {
Log.e("$TAG Failed to save vCard string as file in cache folder") Log.e("$TAG Failed to save vCard string as file in cache folder")
} }

View file

@ -23,7 +23,11 @@ import androidx.annotation.UiThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.File
import java.util.ArrayList import java.util.ArrayList
import java.util.Locale
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsListener import org.linphone.contacts.ContactsListener
@ -34,6 +38,8 @@ import org.linphone.core.SearchResult
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.model.isInSecureMode import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.Event
import org.linphone.utils.FileUtils
class ContactsListViewModel @UiThread constructor() : ViewModel() { class ContactsListViewModel @UiThread constructor() : ViewModel() {
companion object { companion object {
@ -48,6 +54,10 @@ class ContactsListViewModel @UiThread constructor() : ViewModel() {
val isListFiltered = MutableLiveData<Boolean>() val isListFiltered = MutableLiveData<Boolean>()
val vCardTerminatedEvent: MutableLiveData<Event<Pair<String, File>>> by lazy {
MutableLiveData<Event<Pair<String, File>>>()
}
private var currentFilter = "" private var currentFilter = ""
private var previousFilter = "NotSet" private var previousFilter = "NotSet"
private var limitSearchToLinphoneAccounts = true private var limitSearchToLinphoneAccounts = true
@ -160,6 +170,33 @@ class ContactsListViewModel @UiThread constructor() : ViewModel() {
} }
} }
@UiThread
fun exportContactAsVCard(friend: Friend) {
coreContext.postOnCoreThread {
val vCard = friend.vcard?.asVcard4String()
if (!vCard.isNullOrEmpty()) {
Log.i("$TAG Friend has been successfully dumped as vCard string")
val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase(
Locale.getDefault()
)
val file = FileUtils.getFileStorageCacheDir(
"$fileName.vcf",
overrideExisting = true
)
viewModelScope.launch {
if (FileUtils.dumpStringToFile(vCard, file)) {
Log.i("$TAG vCard string saved as file in cache folder")
vCardTerminatedEvent.postValue(Event(Pair(friend.name.orEmpty(), file)))
} else {
Log.e("$TAG Failed to save vCard string as file in cache folder")
}
}
} else {
Log.e("$TAG Failed to dump contact as vCard string")
}
}
}
@WorkerThread @WorkerThread
private fun applyFilter( private fun applyFilter(
filter: String, filter: String,

View file

@ -39,6 +39,8 @@ import org.linphone.core.tools.Log
class LinphoneUtils { class LinphoneUtils {
companion object { companion object {
private const val TAG = "[App Utils]"
@AnyThread @AnyThread
fun getFirstLetter(displayName: String): String { fun getFirstLetter(displayName: String): String {
return getInitials(displayName, 1) return getInitials(displayName, 1)
@ -62,7 +64,7 @@ class LinphoneUtils {
) { ) {
val glyph = emoji.process(split[i]) val glyph = emoji.process(split[i])
if (characters > 0) { // Limit initial to 1 emoji only if (characters > 0) { // Limit initial to 1 emoji only
Log.d("[App Utils] We limit initials to one emoji only") Log.d("$TAG We limit initials to one emoji only")
initials = "" initials = ""
} }
initials += glyph initials += glyph
@ -71,7 +73,7 @@ class LinphoneUtils {
initials += split[i][0] initials += split[i][0]
} }
} catch (ise: IllegalStateException) { } catch (ise: IllegalStateException) {
Log.e("[App Utils] Can't call hasEmojiGlyph: $ise") Log.e("$TAG Can't call hasEmojiGlyph: $ise")
initials += split[i][0] initials += split[i][0]
} }