From 5af3a490d4ac86aebc9a57096dc262ed09d2980d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 27 Aug 2023 12:10:32 +0200 Subject: [PATCH] Added contact export as vCard from contacts list --- .../main/contacts/fragment/ContactFragment.kt | 15 ++++--- .../contacts/fragment/ContactsListFragment.kt | 42 +++++++++++++++++-- .../main/contacts/model/ContactAvatarModel.kt | 2 + .../contacts/viewmodel/ContactViewModel.kt | 11 +++-- .../viewmodel/ContactsListViewModel.kt | 37 ++++++++++++++++ .../java/org/linphone/utils/LinphoneUtils.kt | 6 ++- 6 files changed, 99 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 7b0929082..e90775a24 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -93,6 +93,7 @@ class ContactFragment : GenericFragment() { } binding.setShareClickListener { + Log.i("$TAG Sharing friend, exporting it as vCard file first") viewModel.exportContactAsVCard() } @@ -173,9 +174,13 @@ class ContactFragment : GenericFragment() { } viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) { - it.consume { file -> - Log.i("$TAG Friend was exported as vCard file [${file.absolutePath}]") - shareContact(file) + 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) } } @@ -203,7 +208,7 @@ class ContactFragment : GenericFragment() { ) } - private fun shareContact(file: File) { + private fun shareContact(name: String, file: File) { val publicUri = FileProvider.getUriForFile( requireContext(), requireContext().getString(R.string.file_provider), @@ -214,7 +219,7 @@ class ContactFragment : GenericFragment() { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND 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 } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 4d6b63155..a577838b0 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -19,17 +19,21 @@ */ package org.linphone.ui.main.contacts.fragment +import android.content.Intent import android.os.Bundle +import android.provider.ContactsContract import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils import androidx.annotation.UiThread +import androidx.core.content.FileProvider import androidx.core.view.doOnPreDraw import androidx.navigation.fragment.findNavController import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.LinearLayoutManager +import java.io.File import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R 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") } + 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) { it.consume { filter -> listViewModel.applyFilter(filter) @@ -130,7 +145,7 @@ class ContactsListFragment : GenericFragment() { adapter.contactLongClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> val modalBottomSheet = ContactsListMenuDialogFragment( - model.friend.starred, + model.starred, { // onDismiss adapter.resetSelection() }, @@ -147,8 +162,10 @@ class ContactsListFragment : GenericFragment() { } }, { // onShare - Log.i("$TAG Sharing friend [${model.name.value}]") - // TODO + Log.i( + "$TAG Sharing friend [${model.name.value}], exporting it as vCard file first" + ) + listViewModel.exportContactAsVCard(model.friend) }, { // onDelete 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) + } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index a44611730..b68c21de1 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -37,6 +37,8 @@ class ContactAvatarModel @WorkerThread constructor(val friend: Friend) { val id = friend.refKey + val starred = friend.starred + val avatar = MutableLiveData() val initials = LinphoneUtils.getInitials(friend.name.orEmpty()) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 97c63cc0a..0d3cdd86e 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -80,8 +80,8 @@ class ContactViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } - val vCardTerminatedEvent: MutableLiveData> by lazy { - MutableLiveData>() + val vCardTerminatedEvent: MutableLiveData>> by lazy { + MutableLiveData>>() } val displayTrustProcessDialogEvent: MutableLiveData> by lazy { @@ -287,11 +287,14 @@ class ContactViewModel @UiThread constructor() : ViewModel() { val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase( Locale.getDefault() ) - val file = FileUtils.getFileStorageCacheDir("$fileName.vcf") + 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(file)) + vCardTerminatedEvent.postValue(Event(Pair(friend.name.orEmpty(), file))) } else { Log.e("$TAG Failed to save vCard string as file in cache folder") } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index ea46897b9..7a84705b0 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -23,7 +23,11 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import java.io.File import java.util.ArrayList +import java.util.Locale +import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.contacts.ContactsListener @@ -34,6 +38,8 @@ import org.linphone.core.SearchResult import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.model.isInSecureMode +import org.linphone.utils.Event +import org.linphone.utils.FileUtils class ContactsListViewModel @UiThread constructor() : ViewModel() { companion object { @@ -48,6 +54,10 @@ class ContactsListViewModel @UiThread constructor() : ViewModel() { val isListFiltered = MutableLiveData() + val vCardTerminatedEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + private var currentFilter = "" private var previousFilter = "NotSet" 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 private fun applyFilter( filter: String, diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 2f335de4d..a41964c3b 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -39,6 +39,8 @@ import org.linphone.core.tools.Log class LinphoneUtils { companion object { + private const val TAG = "[App Utils]" + @AnyThread fun getFirstLetter(displayName: String): String { return getInitials(displayName, 1) @@ -62,7 +64,7 @@ class LinphoneUtils { ) { val glyph = emoji.process(split[i]) 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 += glyph @@ -71,7 +73,7 @@ class LinphoneUtils { initials += split[i][0] } } catch (ise: IllegalStateException) { - Log.e("[App Utils] Can't call hasEmojiGlyph: $ise") + Log.e("$TAG Can't call hasEmojiGlyph: $ise") initials += split[i][0] }