diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index b042f8f0c..f189b27e3 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -88,10 +88,14 @@ class ContactsManager { // UI thread coreContext.postOnCoreThread { updateLocalContacts() + notifyContactsListChanged() + } + } - for (listener in listeners) { - listener.onContactsLoaded() - } + fun notifyContactsListChanged() { + // Core thread + for (listener in listeners) { + listener.onContactsLoaded() } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt index 2a2fdc754..a33a05e94 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt @@ -75,13 +75,12 @@ class EditContactFragment : GenericFragment() { } viewModel.saveChangesEvent.observe(viewLifecycleOwner) { - it.consume { ok -> - if (ok) { + it.consume { refKey -> + if (refKey.isNotEmpty()) { Log.i("$TAG Changes were applied, going back to details page") goBack() } else { - Log.e("$TAG Changes couldn't be applied!") - // TODO FIXME : show error + // TODO : show error } } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt index 4390fbcc1..918ca1128 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt @@ -29,6 +29,7 @@ import org.linphone.R import org.linphone.databinding.ContactNewOrEditFragmentBinding import org.linphone.ui.main.contacts.viewmodel.ContactNewOrEditViewModel import org.linphone.ui.main.fragment.GenericFragment +import org.linphone.utils.Event class NewContactFragment : GenericFragment() { private lateinit var binding: ContactNewOrEditFragmentBinding @@ -56,14 +57,17 @@ class NewContactFragment : GenericFragment() { binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel + viewModel.findFriendByRefKey("") + binding.setCancelClickListener { goBack() } viewModel.saveChangesEvent.observe(viewLifecycleOwner) { - it.consume { ok -> - if (ok) { - goBack() // TODO FIXME : go to contact detail view + it.consume { refKey -> + if (refKey.isNotEmpty()) { + goBack() + sharedViewModel.showContactEvent.value = Event(refKey) } else { // TODO : show error } 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 1188e7be8..756f6e984 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 @@ -83,14 +83,18 @@ class ContactAvatarModel(val friend: Friend) { val refKey = friend.refKey if (refKey != null) { - val lookupUri = ContentUris.withAppendedId( - ContactsContract.Contacts.CONTENT_URI, - refKey.toLong() - ) - return Uri.withAppendedPath( - lookupUri, - ContactsContract.Contacts.Photo.CONTENT_DIRECTORY - ) + try { + val lookupUri = ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + refKey.toLong() + ) + return Uri.withAppendedPath( + lookupUri, + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ) + } catch (numberFormatException: NumberFormatException) { + // Expected for contacts created by Linphone + } } return null diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/NewOrEditNumberOrAddressModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/NewOrEditNumberOrAddressModel.kt new file mode 100644 index 000000000..824f1d4d8 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/NewOrEditNumberOrAddressModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.main.contacts.model + +import androidx.lifecycle.MutableLiveData + +class NewOrEditNumberOrAddressModel( + defaultValue: String, + val isSip: Boolean, + private val onValueNoLongerEmpty: (() -> Unit)? = null, + private val onRemove: ((model: NewOrEditNumberOrAddressModel) -> Unit)? = null +) { + val value = MutableLiveData() + + val showRemoveButton = MutableLiveData() + + init { + // Core thread + value.postValue(defaultValue) + showRemoveButton.postValue(defaultValue.isNotEmpty()) + } + + fun onValueChanged(newValue: String) { + // UI thread + if (newValue.isNotEmpty() && showRemoveButton.value == false) { + onValueNoLongerEmpty?.invoke() + showRemoveButton.value = true + } + } + + fun remove() { + // Core thread + onRemove?.invoke(this) + } +} diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index 7077e33b6..18f47ec18 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -25,6 +25,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Friend import org.linphone.core.FriendList.Status import org.linphone.core.tools.Log +import org.linphone.ui.main.contacts.model.NewOrEditNumberOrAddressModel import org.linphone.utils.Event class ContactNewOrEditViewModel() : ViewModel() { @@ -40,12 +41,16 @@ class ContactNewOrEditViewModel() : ViewModel() { val lastName = MutableLiveData() + val sipAddresses = MutableLiveData>() + + val phoneNumbers = MutableLiveData>() + val company = MutableLiveData() val jobTitle = MutableLiveData() - val saveChangesEvent: MutableLiveData> by lazy { - MutableLiveData>() + val saveChangesEvent: MutableLiveData> by lazy { + MutableLiveData>() } val friendFoundEvent = MutableLiveData>() @@ -61,6 +66,9 @@ class ContactNewOrEditViewModel() : ViewModel() { val exists = !friend.refKey.isNullOrEmpty() isEdit.postValue(exists) + val addresses = arrayListOf() + val numbers = arrayListOf() + if (exists) { Log.i("$TAG Found friend [$friend] using ref key [$refKey]") val vCard = friend.vcard @@ -68,17 +76,49 @@ class ContactNewOrEditViewModel() : ViewModel() { firstName.postValue(vCard.givenName) lastName.postValue(vCard.familyName) } else { - // TODO + // TODO ? + } + + for (address in friend.addresses) { + addresses.add( + NewOrEditNumberOrAddressModel(address.asStringUriOnly(), true, { }, { model -> + removeModel(model) + }) + ) + } + for (number in friend.phoneNumbers) { + numbers.add( + NewOrEditNumberOrAddressModel(number, false, { }, { model -> + removeModel(model) + }) + ) } company.postValue(friend.organization) jobTitle.postValue(friend.jobTitle) friendFoundEvent.postValue(Event(true)) - } else { + } else if (refKey.orEmpty().isNotEmpty()) { Log.e("$TAG No friend found using ref key [$refKey]") - // TODO : generate unique ref key } + + addresses.add( + NewOrEditNumberOrAddressModel("", true, { + addNewModel(true) + }, { model -> + removeModel(model) + }) + ) + numbers.add( + NewOrEditNumberOrAddressModel("", false, { + addNewModel(false) + }, { model -> + removeModel(model) + }) + ) + + sipAddresses.postValue(addresses) + phoneNumbers.postValue(numbers) } } @@ -87,26 +127,107 @@ class ContactNewOrEditViewModel() : ViewModel() { coreContext.postOnCoreThread { core -> var status = Status.OK - if (::friend.isInitialized) { - friend.name = "${firstName.value.orEmpty()} ${lastName.value.orEmpty()}" - - val vCard = friend.vcard - if (vCard != null) { - vCard.familyName = lastName.value - vCard.givenName = firstName.value - } - - friend.organization = company.value.orEmpty() - friend.jobTitle = jobTitle.value.orEmpty() - - if (isEdit.value == false) { - status = core.defaultFriendList?.addFriend(friend) ?: Status.InvalidFriend - } - } else { - status = Status.NonExistentFriend + if (!::friend.isInitialized) { + friend = core.createFriend() } - saveChangesEvent.postValue(Event(status == Status.OK)) + if (isEdit.value == true) { + friend.edit() + } + + friend.name = "${firstName.value.orEmpty()} ${lastName.value.orEmpty()}" + + val vCard = friend.vcard + if (vCard != null) { + vCard.familyName = lastName.value + vCard.givenName = firstName.value + } + + friend.organization = company.value.orEmpty() + friend.jobTitle = jobTitle.value.orEmpty() + + for (address in friend.addresses) { + friend.removeAddress(address) + } + for (address in sipAddresses.value.orEmpty()) { + val data = address.value.value + if (!data.isNullOrEmpty()) { + val parsedAddress = core.interpretUrl(data, true) + if (parsedAddress != null) { + friend.addAddress(parsedAddress) + } + } + } + + for (number in friend.phoneNumbers) { + friend.removePhoneNumber(number) + } + for (number in phoneNumbers.value.orEmpty()) { + val data = number.value.value + if (!data.isNullOrEmpty()) { + friend.addPhoneNumber(data) + } + } + + if (isEdit.value == false) { + if (friend.vcard?.generateUniqueId() == true) { + friend.refKey = friend.vcard?.uid + Log.i( + "$TAG Newly created friend will have generated ref key [${friend.refKey}]" + ) + } else { + Log.e("$TAG Failed to generate a ref key using vCard's generateUniqueId()") + // TODO : generate unique ref key + } + status = core.defaultFriendList?.addFriend(friend) ?: Status.InvalidFriend + } else { + friend.done() + } + coreContext.contactsManager.notifyContactsListChanged() + + saveChangesEvent.postValue( + Event(if (status == Status.OK) friend.refKey.orEmpty() else "") + ) + } + } + + private fun addNewModel(isSip: Boolean) { + // UI thread + // TODO FIXME: causes focus issues + val list = arrayListOf() + val source = if (isSip) sipAddresses.value.orEmpty() else phoneNumbers.value.orEmpty() + + list.addAll(source) + list.add( + NewOrEditNumberOrAddressModel("", isSip, { + addNewModel(isSip) + }, { model -> + removeModel(model) + }) + ) + + if (isSip) { + sipAddresses.value = list + } else { + phoneNumbers.value = list + } + } + + private fun removeModel(model: NewOrEditNumberOrAddressModel) { + // UI thread + val list = arrayListOf() + val source = if (model.isSip) sipAddresses.value.orEmpty() else phoneNumbers.value.orEmpty() + + for (item in source) { + if (item != model) { + list.add(item) + } + } + + if (model.isSip) { + sipAddresses.value = list + } else { + phoneNumbers.value = list } } } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index a5ee587fa..17ba23741 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -20,12 +20,15 @@ package org.linphone.utils import android.content.Context +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.Window import android.view.inputmethod.InputMethodManager import android.widget.ImageView +import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat @@ -174,3 +177,16 @@ fun AvatarView.loadContactPicture(contact: ContactAvatarModel?) { ) } } + +@BindingAdapter("onValueChanged") +fun AppCompatEditText.editTextSetting(lambda: () -> Unit) { + addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + lambda() + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) +} diff --git a/app/src/main/res/drawable/shape_edit_text_background.xml b/app/src/main/res/drawable/shape_edit_text_background.xml index da6dc5ae7..162c2b8e7 100644 --- a/app/src/main/res/drawable/shape_edit_text_background.xml +++ b/app/src/main/res/drawable/shape_edit_text_background.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index 0e80bf3bd..3d481792d 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -110,7 +110,7 @@ android:layout_height="wrap_content" android:layout_marginStart="4dp" android:layout_marginEnd="4dp" - android:layout_marginTop="8dp" + android:layout_marginTop="4dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintTop_toBottomOf="@id/favourites_label" /> diff --git a/app/src/main/res/layout/contact_favourite_list_cell.xml b/app/src/main/res/layout/contact_favourite_list_cell.xml index 65e235026..74649347b 100644 --- a/app/src/main/res/layout/contact_favourite_list_cell.xml +++ b/app/src/main/res/layout/contact_favourite_list_cell.xml @@ -20,10 +20,9 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index fdb896dd2..6410b4a92 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -164,7 +164,29 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> - + + + + app:layout_constraintTop_toBottomOf="@id/phone_numbers"/>