From ab15d05ffd01e2872ea6c5190b0c735de2dd4254 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 18 Aug 2023 13:50:53 +0200 Subject: [PATCH] Added sharing/removal of contact --- .../assistant/viewmodel/AssistantViewModel.kt | 8 +++ .../main/contacts/fragment/ContactFragment.kt | 42 ++++++++++++ .../contacts/viewmodel/ContactViewModel.kt | 66 +++++++++++++++++-- .../main/java/org/linphone/utils/FileUtils.kt | 33 ++++++++++ app/src/main/res/drawable/favourite.xml | 13 ++++ app/src/main/res/drawable/hide_password.xml | 13 ++++ .../{favorite.xml => not_favourite.xml} | 0 .../res/layout/assistant_login_fragment.xml | 17 ++++- app/src/main/res/layout/contact_fragment.xml | 39 ++++------- .../res/layout/contact_new_or_edit_cell.xml | 2 +- .../layout/contacts_list_long_press_menu.xml | 12 ++-- 11 files changed, 204 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/drawable/favourite.xml create mode 100644 app/src/main/res/drawable/hide_password.xml rename app/src/main/res/drawable/{favorite.xml => not_favourite.xml} (100%) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt index cae0c3704..37181241f 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt @@ -41,6 +41,8 @@ class AssistantViewModel : ViewModel() { val password = MutableLiveData() + val showPassword = MutableLiveData() + val loginEnabled = MediatorLiveData() val registrationInProgress = MutableLiveData() @@ -84,6 +86,7 @@ class AssistantViewModel : ViewModel() { } init { + showPassword.value = false registrationInProgress.value = false loginEnabled.addSource(username) { @@ -125,6 +128,11 @@ class AssistantViewModel : ViewModel() { } } + fun toggleShowPassword() { + // UI thread + showPassword.value = showPassword.value == false + } + private fun isLoginButtonEnabled(): Boolean { return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() } 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 1356ad677..1dadcadd3 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 @@ -29,11 +29,13 @@ import android.provider.ContactsContract import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.FileProvider import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import java.io.File import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.ContactFragmentBinding @@ -45,6 +47,10 @@ import org.linphone.utils.DialogUtils import org.linphone.utils.Event class ContactFragment : GenericFragment() { + companion object { + const val TAG = "[Contact Fragment]" + } + private lateinit var binding: ContactFragmentBinding private lateinit var viewModel: ContactViewModel @@ -82,6 +88,16 @@ class ContactFragment : GenericFragment() { goBack() } + binding.setShareClickListener { + viewModel.exportContactAsVCard() + } + + binding.setDeleteClickListener { + viewModel.deleteContact() + goBack() + // TODO: show toast ? + } + sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable -> viewModel.showBackButton.value = slideable } @@ -143,6 +159,13 @@ class ContactFragment : GenericFragment() { findNavController().navigate(action) } } + + viewModel.vCardTerminatedEvent.observe(viewLifecycleOwner) { + it.consume { file -> + Log.i("$TAG Friend was exported as vCard file [${file.absolutePath}]") + shareContact(file) + } + } } private fun copyNumberOrAddressToClipboard(value: String, isSip: Boolean) { @@ -155,4 +178,23 @@ class ContactFragment : GenericFragment() { R.drawable.check ) } + + private fun shareContact(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, "John Doe") + 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/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 5634bf760..feba091f8 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 @@ -21,16 +21,26 @@ package org.linphone.ui.main.contacts.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import java.io.File +import java.util.Locale +import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Address import org.linphone.core.Friend +import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactDeviceModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.utils.Event +import org.linphone.utils.FileUtils class ContactViewModel : ViewModel() { + companion object { + const val TAG = "[Contact ViewModel]" + } + val contact = MutableLiveData() val sipAddressesAndPhoneNumbers = MutableLiveData>() @@ -67,6 +77,10 @@ class ContactViewModel : ViewModel() { MutableLiveData>() } + val vCardTerminatedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + private val listener = object : ContactNumberOrAddressClickListener { override fun onClicked(address: Address?) { // UI thread @@ -181,12 +195,52 @@ class ContactViewModel : ViewModel() { fun editContact() { // UI thread - val uri = contact.value?.friend?.nativeUri - if (uri != null) { - openLinphoneContactEditor.value = Event(contact.value?.id.orEmpty()) - // TODO FIXME : openNativeContactEditor.value = Event(uri) - } else { - openLinphoneContactEditor.value = Event(contact.value?.id.orEmpty()) + coreContext.postOnCoreThread { + if (::friend.isInitialized) { + val uri = friend.nativeUri + if (uri != null) { + openNativeContactEditor.postValue(Event(uri)) + } else { + openLinphoneContactEditor.postValue(Event(contact.value?.id.orEmpty())) + } + } + } + } + + fun exportContactAsVCard() { + // UI thread + coreContext.postOnCoreThread { + if (::friend.isInitialized) { + 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") + viewModelScope.launch { + if (FileUtils.dumpStringToFile(vCard, file)) { + Log.i("$TAG vCard string saved as file in cache folder") + vCardTerminatedEvent.postValue(Event(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") + } + } + } + } + + fun deleteContact() { + // UI thread + coreContext.postOnCoreThread { core -> + if (::friend.isInitialized) { + Log.i("$TAG Deleting friend [$friend]") + friend.remove() + coreContext.contactsManager.notifyContactsListChanged() + } } } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 239105ddf..ec4acfeca 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -47,6 +47,20 @@ class FileUtils { return file } + fun getFileStorageCacheDir(fileName: String): File { + val path = coreContext.context.cacheDir + Log.i("$TAG Cache directory is: $path") + + var file = File(path, fileName) + var prefix = 1 + while (file.exists()) { + file = File(path, prefix.toString() + "_" + fileName) + Log.w("$TAG File with that name already exists, renamed to ${file.name}") + prefix += 1 + } + return file + } + suspend fun copyFile(from: Uri, to: File): Boolean { try { withContext(Dispatchers.IO) { @@ -68,6 +82,25 @@ class FileUtils { return false } + suspend fun dumpStringToFile(data: String, to: File): Boolean { + try { + withContext(Dispatchers.IO) { + FileOutputStream(to).use { outputStream -> + val inputStream = data.byteInputStream() + val buffer = ByteArray(4096) + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } >= 0) { + outputStream.write(buffer, 0, bytesRead) + } + } + } + return true + } catch (e: IOException) { + Log.e("$TAG dumpStringToFile [$data] to [$to] exception: $e") + } + return false + } + private fun getFileStorageDir(isPicture: Boolean = false): File { var path: File? = null if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { diff --git a/app/src/main/res/drawable/favourite.xml b/app/src/main/res/drawable/favourite.xml new file mode 100644 index 000000000..0f2c1c2a6 --- /dev/null +++ b/app/src/main/res/drawable/favourite.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/hide_password.xml b/app/src/main/res/drawable/hide_password.xml new file mode 100644 index 000000000..1c86284e6 --- /dev/null +++ b/app/src/main/res/drawable/hide_password.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/favorite.xml b/app/src/main/res/drawable/not_favourite.xml similarity index 100% rename from app/src/main/res/drawable/favorite.xml rename to app/src/main/res/drawable/not_favourite.xml diff --git a/app/src/main/res/layout/assistant_login_fragment.xml b/app/src/main/res/layout/assistant_login_fragment.xml index deb4b018f..98210ca19 100644 --- a/app/src/main/res/layout/assistant_login_fragment.xml +++ b/app/src/main/res/layout/assistant_login_fragment.xml @@ -5,6 +5,7 @@ + @@ -108,13 +109,23 @@ android:textSize="14sp" android:textColor="@color/gray_9" android:background="@drawable/shape_edit_text_background" - android:inputType="textPassword" - android:drawableEnd="@drawable/show_password" - android:drawableTint="@color/gray_1" + android:inputType="@{viewModel.showPassword ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}" app:layout_constraintTop_toBottomOf="@id/password_label" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> + + + + @@ -491,9 +497,9 @@ android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" - android:text="@{viewModel.isFavourite() ? `Remove from favourites` : `Add to favourites`, default=`Add to favourites`}" - android:drawableStart="@drawable/favorite" - android:drawableTint="@{viewModel.isFavourite() ? @color/red_danger : @color/gray_1, default=@color/gray_1}" + android:text="@{viewModel.isFavourite ? `Remove from favourites` : `Add to favourites`, default=`Add to favourites`}" + android:drawableStart="@{viewModel.isFavourite ? @drawable/favourite : @drawable/not_favourite, default=@drawable/favourite}" + android:drawableTint="@{viewModel.isFavourite ? @color/red_danger : @color/gray_1, default=@color/gray_1}" app:layout_constraintTop_toBottomOf="@id/action_edit" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> @@ -509,6 +515,7 @@ app:layout_constraintTop_toBottomOf="@+id/action_favorite"/> - - - - diff --git a/app/src/main/res/layout/contact_new_or_edit_cell.xml b/app/src/main/res/layout/contact_new_or_edit_cell.xml index 6397f1a0f..cb8650eb7 100644 --- a/app/src/main/res/layout/contact_new_or_edit_cell.xml +++ b/app/src/main/res/layout/contact_new_or_edit_cell.xml @@ -46,7 +46,7 @@ android:textColor="@color/gray_9" android:background="@drawable/shape_edit_text_background" android:maxLines="1" - android:inputType="@{model.isSip ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE}" + android:inputType="@{model.isSip ? InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS : InputType.TYPE_CLASS_PHONE}" app:layout_constraintTop_toBottomOf="@id/label" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/remove"/> diff --git a/app/src/main/res/layout/contacts_list_long_press_menu.xml b/app/src/main/res/layout/contacts_list_long_press_menu.xml index a1adbb4f6..31b2147ec 100644 --- a/app/src/main/res/layout/contacts_list_long_press_menu.xml +++ b/app/src/main/res/layout/contacts_list_long_press_menu.xml @@ -16,6 +16,9 @@ +