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"/>