Added LDAP settings, using MagicSearch in contacts list + updated CHANGELOG from 4.6.2 release

This commit is contained in:
Sylvain Berfini 2022-02-11 11:36:29 +01:00
parent e34965d524
commit 62e2fb580f
20 changed files with 993 additions and 34 deletions

View file

@ -10,10 +10,18 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
## [4.7.0] - Unreleased
## [4.6.2] - Unreleased
### Added
-
### Changed
-
### Fixed
-
## [4.6.2] - 2022-03-01
### Added
- Request BLUETOOTH_CONNECT permission on Android 12+ devices, if not we won't be notified when a BT device is being connected/disconnected while app is alive.
@ -23,13 +31,15 @@ Group changes to describe their impact on the project, as follows:
- Prevent screen to turn off while recording a voice message
### Changed
- Contacts lists now show LDAP contacts if any, as well as "generated" contacts from SIP addresses you have interacted with
- Contacts lists now show LDAP contacts if any
### Fixed
- Negative gain in audio settings is allowed again
- STUN server URL setting not enabling it for non sip.linphone.org accounts
- Contacts list header case comparison
- Stop voice recording playback when sending chat message
- Call activity not finishing when hanging up sometimes
- Auto start setting disabled not working if background mode setting was enabled
## [4.6.1] - 2022-02-14

View file

@ -866,6 +866,17 @@ internal fun navigateToEmptySetting(navController: NavController) {
)
}
internal fun ContactsSettingsFragment.navigateToLdapSettings(configIndex: Int) {
if (findNavController().currentDestination?.id == R.id.contactsSettingsFragment) {
val bundle = bundleOf("LdapConfigIndex" to configIndex)
findNavController().navigate(
R.id.action_contactsSettingsFragment_to_ldapSettingsFragment,
bundle,
popupTo()
)
}
}
internal fun AccountSettingsFragment.navigateToEmptySetting() {
navigateToEmptySetting(findNavController())
}

View file

@ -36,6 +36,7 @@ import com.google.android.material.transition.MaterialSharedAxis
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.SnackBarActivity
import org.linphone.activities.clearDisplayedContact
import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.contact.adapters.ContactsListAdapter
@ -172,6 +173,14 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
val viewModel = DialogViewModel(getString(R.string.contact_delete_one_dialog))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
val contactViewModel = adapter.currentList[viewHolder.bindingAdapterPosition]
if (contactViewModel.isNativeContact.value == false) {
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.contact_cant_be_deleted)
return
}
viewModel.showCancelButton {
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
dialog.dismiss()
@ -211,6 +220,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
it.consume { contact ->
Log.i("[Contacts] Selected item in list changed: $contact")
sharedViewModel.selectedContact.value = contact
(requireActivity() as MainActivity).hideKeyboard()
if (editOnClick) {
navigateToContactEditor(sipUriToAdd, binding.slidingPane)
@ -237,6 +247,14 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
adapter.submitList(it)
}
listViewModel.moreResultsAvailableEvent.observe(
viewLifecycleOwner
) {
it.consume {
(requireActivity() as SnackBarActivity).showSnackBar(R.string.contacts_ldap_query_more_results_available)
}
}
binding.setAllContactsToggleClickListener {
listViewModel.sipContactsSelected.value = false
}
@ -247,13 +265,13 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
listViewModel.sipContactsSelected.observe(
viewLifecycleOwner
) {
listViewModel.updateContactsList()
listViewModel.updateContactsList(true)
}
listViewModel.filter.observe(
viewLifecycleOwner
) {
listViewModel.updateContactsList()
listViewModel.updateContactsList(false)
}
binding.setNewContactClickListener {

View file

@ -74,6 +74,8 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
val waitForChatRoomCreation = MutableLiveData<Boolean>()
val isNativeContact = MutableLiveData<Boolean>()
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
override fun onContactUpdated(contact: Contact) {
if (contact is NativeContact && contactInternal is NativeContact && contact.nativeId == contactInternal.nativeId) {
@ -127,6 +129,7 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
init {
contact.value = contactInternal
displayName.value = contactInternal.fullName ?: contactInternal.firstName + " " + contactInternal.lastName
isNativeContact.value = contactInternal is NativeContact
updateNumbersAndAddresses(contactInternal)
coreContext.contactsManager.addListener(contactsUpdatedListener)
@ -172,7 +175,7 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
}
}
private fun updateNumbersAndAddresses(contact: Contact) {
fun updateNumbersAndAddresses(contact: Contact) {
val list = arrayListOf<ContactNumberOrAddressData>()
for (address in contact.sipAddresses) {
val value = address.asStringUriOnly()

View file

@ -23,61 +23,125 @@ import android.content.ContentProviderOperation
import android.provider.ContactsContract
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.util.*
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contact.Contact
import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.contact.NativeContact
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class ContactsListViewModel : ViewModel() {
val sipContactsSelected = MutableLiveData<Boolean>()
val contactsList = MutableLiveData<ArrayList<ContactViewModel>>()
val fetchInProgress = MutableLiveData<Boolean>()
private var searchResultsPending: Boolean = false
private var fastFetchJob: Job? = null
val filter = MutableLiveData<String>()
private var previousFilter = "NotSet"
val moreResultsAvailableEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
override fun onContactsUpdated() {
Log.i("[Contacts] Contacts have changed")
updateContactsList()
updateContactsList(true)
}
}
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
searchResultsPending = false
processMagicSearchResults(magicSearch.lastSearch)
fetchInProgress.value = false
}
override fun onLdapHaveMoreResults(magicSearch: MagicSearch, ldap: Ldap) {
moreResultsAvailableEvent.value = Event(true)
}
}
init {
sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList()
fetchInProgress.value = false
coreContext.contactsManager.addListener(contactsUpdatedListener)
coreContext.contactsManager.magicSearch.addListener(magicSearchListener)
}
override fun onCleared() {
contactsList.value.orEmpty().forEach(ContactViewModel::destroy)
coreContext.contactsManager.magicSearch.removeListener(magicSearchListener)
coreContext.contactsManager.removeListener(contactsUpdatedListener)
super.onCleared()
}
private fun getSelectedContactsList(): ArrayList<ContactViewModel> {
val list = arrayListOf<ContactViewModel>()
val source =
if (sipContactsSelected.value == true) coreContext.contactsManager.sipContacts
else coreContext.contactsManager.contacts
for (contact in source) {
list.add(ContactViewModel(contact))
}
return list
}
fun updateContactsList() {
val list: ArrayList<ContactViewModel>
fun updateContactsList(clearCache: Boolean) {
val filterValue = filter.value.orEmpty()
list = if (filterValue.isNotEmpty()) {
getSelectedContactsList().filter { contact ->
contact.name.contains(filterValue, true)
} as ArrayList<ContactViewModel>
contactsList.value.orEmpty().forEach(ContactViewModel::destroy)
if (clearCache || (
previousFilter.isNotEmpty() && (
previousFilter.length > filterValue.length ||
(previousFilter.length == filterValue.length && previousFilter != filterValue)
)
)
) {
coreContext.contactsManager.magicSearch.resetSearchCache()
}
previousFilter = filterValue
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else ""
val filter = MagicSearchSource.Friends.toInt() or MagicSearchSource.LdapServers.toInt()
searchResultsPending = true
fastFetchJob?.cancel()
coreContext.contactsManager.magicSearch.getContactsAsync(filterValue, domain, filter)
fastFetchJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
delay(200)
withContext(Dispatchers.Main) {
if (searchResultsPending) {
fetchInProgress.value = true
}
}
}
}
}
private fun processMagicSearchResults(results: Array<SearchResult>) {
val list = arrayListOf<ContactViewModel>()
for (result in results) {
val contact = searchMatchingContact(result) ?: Contact(searchResult = result)
if (contact is NativeContact) {
val found = list.find { contactViewModel ->
contactViewModel.contactInternal is NativeContact && contactViewModel.contactInternal.nativeId == contact.nativeId
}
if (found != null) {
Log.d("[Contacts] Found a search result that matches a native contact [$contact] we already have, skipping")
continue
}
} else {
getSelectedContactsList()
val found = list.find { contactViewModel ->
contactViewModel.displayName.value == contact.fullName
}
if (found != null) {
Log.i("[Contacts] Found a search result that matches a contact [$contact] we already have, updating it with the new information")
found.contactInternal.addAddressAndPhoneNumberFromSearchResult(result)
found.updateNumbersAndAddresses(found.contactInternal)
continue
}
}
list.add(ContactViewModel(contact))
}
contactsList.postValue(list)
@ -146,4 +210,19 @@ class ContactsListViewModel : ViewModel() {
}
}
}
private fun searchMatchingContact(searchResult: SearchResult): Contact? {
val address = searchResult.address
if (address != null) {
val contact = coreContext.contactsManager.findContactByAddress(address, ignoreLocalContact = true)
if (contact != null) return contact
}
if (searchResult.phoneNumber != null) {
return coreContext.contactsManager.findContactByPhoneNumber(searchResult.phoneNumber.orEmpty())
}
return null
}
}

View file

@ -26,8 +26,10 @@ import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.activities.main.settings.viewmodels.ContactsSettingsViewModel
import org.linphone.activities.navigateToEmptySetting
import org.linphone.activities.navigateToLdapSettings
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.SettingsContactsFragmentBinding
@ -74,6 +76,22 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
}
}
viewModel.ldapNewSettingsListener = object : SettingListenerStub() {
override fun onClicked() {
Log.i("[Contacts Settings] Clicked on new LDAP config")
navigateToLdapSettings(-1)
}
}
viewModel.ldapSettingsClickedEvent.observe(
viewLifecycleOwner
) {
it.consume { index ->
Log.i("[Contacts Settings] Clicked on LDAP config with index: $index")
navigateToLdapSettings(index)
}
}
if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) {
Log.i("[Contacts Settings] Asking for READ_CONTACTS permission")
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
@ -117,4 +135,9 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
navigateToEmptySetting()
}
}
override fun onResume() {
super.onResume()
viewModel.updateLdapConfigurationsList()
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2010-2022 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.main.settings.fragments
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModel
import org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModelFactory
import org.linphone.core.tools.Log
import org.linphone.databinding.SettingsLdapFragmentBinding
class LdapSettingsFragment : GenericSettingFragment<SettingsLdapFragmentBinding>() {
private lateinit var viewModel: LdapSettingsViewModel
override fun getLayoutId(): Int = R.layout.settings_ldap_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.sharedMainViewModel = sharedViewModel
val configIndex = arguments?.getInt("LdapConfigIndex")
if (configIndex == null) {
Log.e("[LDAP Settings] Config index not specified!")
goBack()
return
}
try {
viewModel = ViewModelProvider(this, LdapSettingsViewModelFactory(configIndex))[LdapSettingsViewModel::class.java]
} catch (nsee: NoSuchElementException) {
Log.e("[LDAP Settings] Failed to find LDAP object, aborting!")
goBack()
return
}
binding.viewModel = viewModel
viewModel.ldapConfigDeletedEvent.observe(
viewLifecycleOwner
) {
it.consume {
goBack()
}
}
binding.setBackClickListener { goBack() }
}
}

View file

@ -20,6 +20,7 @@
package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
@ -76,6 +77,20 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
val launcherShortcuts = MutableLiveData<Boolean>()
val launcherShortcutsEvent = MutableLiveData<Event<Boolean>>()
val ldapAvailable = MutableLiveData<Boolean>()
val ldapConfigurations = MutableLiveData<ArrayList<LdapSettingsViewModel>>()
lateinit var ldapNewSettingsListener: SettingListenerStub
val ldapSettingsClickedEvent: MutableLiveData<Event<Int>> by lazy {
MutableLiveData<Event<Int>>()
}
private var ldapSettingsListener = object : SettingListenerStub() {
override fun onAccountClicked(identity: String) {
ldapSettingsClickedEvent.value = Event(identity.toInt())
}
}
init {
readContactsPermissionGranted.value = PermissionHelper.get().hasReadContactsPermission()
@ -84,5 +99,23 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
nativePresence.value = prefs.storePresenceInNativeContact
showOrganization.value = prefs.displayOrganization
launcherShortcuts.value = prefs.contactsShortcuts
ldapAvailable.value = core.ldapAvailable()
ldapConfigurations.value = arrayListOf()
updateLdapConfigurationsList()
}
fun updateLdapConfigurationsList() {
val list = arrayListOf<LdapSettingsViewModel>()
var index = 0
for (ldap in coreContext.core.ldapList) {
val viewModel = LdapSettingsViewModel(ldap, index.toString())
viewModel.ldapSettingsListener = ldapSettingsListener
list.add(viewModel)
index += 1
}
ldapConfigurations.value = list
}
}

View file

@ -0,0 +1,296 @@
/*
* Copyright (c) 2010-2022 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.lang.NumberFormatException
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.Ldap
import org.linphone.core.LdapAuthMethod
import org.linphone.core.LdapCertVerificationMode
import org.linphone.core.LdapDebugLevel
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class LdapSettingsViewModelFactory(private val index: Int) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (index >= 0 && index <= coreContext.core.ldapList.size) {
val ldap = coreContext.core.ldapList[index]
return LdapSettingsViewModel(ldap, index.toString()) as T
}
val ldapParams = coreContext.core.createLdapParams()
val ldap = coreContext.core.createLdapWithParams(ldapParams)
return LdapSettingsViewModel(ldap, "-1") as T
}
}
class LdapSettingsViewModel(private val ldap: Ldap, val index: String) : GenericSettingsViewModel() {
lateinit var ldapSettingsListener: SettingListenerStub
val ldapConfigDeletedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val ldapEnableListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = ldap.params.clone()
params.enabled = newValue
ldap.params = params
}
}
val ldapEnable = MutableLiveData<Boolean>()
val deleteListener = object : SettingListenerStub() {
override fun onClicked() {
coreContext.core.removeLdap(ldap)
ldapConfigDeletedEvent.value = Event(true)
}
}
val ldapServerListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.server = newValue
ldap.params = params
}
}
val ldapServer = MutableLiveData<String>()
val ldapBindDnListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.bindDn = newValue
ldap.params = params
}
}
val ldapBindDn = MutableLiveData<String>()
val ldapPasswordListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.password = newValue
ldap.params = params
}
}
val ldapPassword = MutableLiveData<String>()
val ldapAuthMethodListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
val params = ldap.params.clone()
params.authMethod = LdapAuthMethod.fromInt(ldapAuthMethodValues[position])
ldap.params = params
ldapAuthMethodIndex.value = position
}
}
val ldapAuthMethodIndex = MutableLiveData<Int>()
val ldapAuthMethodLabels = MutableLiveData<ArrayList<String>>()
private val ldapAuthMethodValues = arrayListOf<Int>()
val ldapTlsListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = ldap.params.clone()
params.isTlsEnabled = newValue
ldap.params = params
}
}
val ldapTls = MutableLiveData<Boolean>()
val ldapCertCheckListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
val params = ldap.params.clone()
params.serverCertificatesVerificationMode = LdapCertVerificationMode.fromInt(ldapCertCheckValues[position])
ldap.params = params
ldapCertCheckIndex.value = position
}
}
val ldapCertCheckIndex = MutableLiveData<Int>()
val ldapCertCheckLabels = MutableLiveData<ArrayList<String>>()
private val ldapCertCheckValues = arrayListOf<Int>()
val ldapSearchBaseListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.baseObject = newValue
ldap.params = params
}
}
val ldapSearchBase = MutableLiveData<String>()
val ldapSearchFilterListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.filter = newValue
ldap.params = params
}
}
val ldapSearchFilter = MutableLiveData<String>()
val ldapSearchMaxResultsListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
try {
val intValue = newValue.toInt()
val params = ldap.params.clone()
params.maxResults = intValue
ldap.params = params
} catch (nfe: NumberFormatException) {
Log.e("[LDAP Settings] Failed to set max results ($newValue): $nfe")
}
}
}
val ldapSearchMaxResults = MutableLiveData<Int>()
val ldapSearchTimeoutListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
try {
val intValue = newValue.toInt()
val params = ldap.params.clone()
params.timeout = intValue
ldap.params = params
} catch (nfe: NumberFormatException) {
Log.e("[LDAP Settings] Failed to set timeout ($newValue): $nfe")
}
}
}
val ldapSearchTimeout = MutableLiveData<Int>()
val ldapRequestDelayListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
try {
val intValue = newValue.toInt()
val params = ldap.params.clone()
params.delay = intValue
ldap.params = params
} catch (nfe: NumberFormatException) {
Log.e("[LDAP Settings] Failed to set request delay ($newValue): $nfe")
}
}
}
val ldapRequestDelay = MutableLiveData<Int>()
val ldapMinimumCharactersListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
try {
val intValue = newValue.toInt()
val params = ldap.params.clone()
params.minChars = intValue
ldap.params = params
} catch (nfe: NumberFormatException) {
Log.e("[LDAP Settings] Failed to set minimum characters ($newValue): $nfe")
}
}
}
val ldapMinimumCharacters = MutableLiveData<Int>()
val ldapNameAttributeListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.nameAttribute = newValue
ldap.params = params
}
}
val ldapNameAttribute = MutableLiveData<String>()
val ldapSipAttributeListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.sipAttribute = newValue
ldap.params = params
}
}
val ldapSipAttribute = MutableLiveData<String>()
val ldapSipDomainListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = ldap.params.clone()
params.sipDomain = newValue
ldap.params = params
}
}
val ldapSipDomain = MutableLiveData<String>()
val ldapDebugListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = ldap.params.clone()
params.debugLevel = if (newValue) LdapDebugLevel.Verbose else LdapDebugLevel.Off
ldap.params = params
}
}
val ldapDebug = MutableLiveData<Boolean>()
init {
val params = ldap.params
ldapEnable.value = params.enabled
ldapServer.value = params.server
ldapBindDn.value = params.bindDn
ldapPassword.value = params.password
ldapTls.value = params.isTlsEnabled
ldapSearchBase.value = params.baseObject
ldapSearchFilter.value = params.filter
ldapSearchMaxResults.value = params.maxResults
ldapSearchTimeout.value = params.timeout
ldapRequestDelay.value = params.delay
ldapMinimumCharacters.value = params.minChars
ldapNameAttribute.value = params.nameAttribute
ldapSipAttribute.value = params.sipAttribute
ldapSipDomain.value = params.sipDomain
ldapDebug.value = params.debugLevel == LdapDebugLevel.Verbose
initAuthMethodList()
initTlsCertCheckList()
}
private fun initAuthMethodList() {
val labels = arrayListOf<String>()
labels.add(prefs.getString(R.string.contacts_settings_ldap_auth_method_anonymous))
ldapAuthMethodValues.add(LdapAuthMethod.Anonymous.toInt())
labels.add(prefs.getString(R.string.contacts_settings_ldap_auth_method_simple))
ldapAuthMethodValues.add(LdapAuthMethod.Simple.toInt())
ldapAuthMethodLabels.value = labels
ldapAuthMethodIndex.value = ldapAuthMethodValues.indexOf(ldap.params.authMethod.toInt())
}
private fun initTlsCertCheckList() {
val labels = arrayListOf<String>()
labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_auto))
ldapCertCheckValues.add(LdapCertVerificationMode.Default.toInt())
labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_disabled))
ldapCertCheckValues.add(LdapCertVerificationMode.Disabled.toInt())
labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_enabled))
ldapCertCheckValues.add(LdapCertVerificationMode.Enabled.toInt())
ldapCertCheckLabels.value = labels
ldapCertCheckIndex.value = ldapCertCheckValues.indexOf(ldap.params.serverCertificatesVerificationMode.toInt())
}
}

View file

@ -29,8 +29,10 @@ import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.Friend
import org.linphone.core.PresenceBasicStatus
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.utils.ImageUtils
import org.linphone.utils.LinphoneUtils
data class PhoneNumber(val value: String, val typeLabel: String) : Comparable<PhoneNumber> {
override fun compareTo(other: PhoneNumber): Int {
@ -38,7 +40,7 @@ data class PhoneNumber(val value: String, val typeLabel: String) : Comparable<Ph
}
}
open class Contact : Comparable<Contact> {
open class Contact() : Comparable<Contact> {
var fullName: String? = null
var firstName: String? = null
var lastName: String? = null
@ -55,6 +57,31 @@ open class Contact : Comparable<Contact> {
private var thumbnailUri: Uri? = null
constructor(searchResult: SearchResult) : this() {
friend = searchResult.friend
addAddressAndPhoneNumberFromSearchResult(searchResult)
}
fun addAddressAndPhoneNumberFromSearchResult(searchResult: SearchResult) {
val address = searchResult.address
if (address != null) {
if (fullName == null) {
fullName = friend?.name ?: LinphoneUtils.getDisplayName(address)
}
sipAddresses.add(address)
}
val phoneNumber = searchResult.phoneNumber
if (phoneNumber != null) {
if (address == null && fullName == null) {
fullName = friend?.name ?: phoneNumber.orEmpty()
}
phoneNumbers.add(PhoneNumber(phoneNumber, ""))
}
}
override fun compareTo(other: Contact): Int {
val fn = fullName ?: ""
val otherFn = other.fullName ?: ""

View file

@ -238,13 +238,15 @@ class ContactsManager(private val context: Context) {
}
@Synchronized
fun findContactByAddress(address: Address): Contact? {
fun findContactByAddress(address: Address, ignoreLocalContact: Boolean = false): Contact? {
if (!ignoreLocalContact) {
val localContact = localAccountsContacts.find { localContact ->
localContact.sipAddresses.find { localAddress ->
address.weakEqual(localAddress)
} != null
}
if (localContact != null) return localContact
}
val cleanAddress = address.clone()
cleanAddress.clean() // To remove gruu if any

View file

@ -52,6 +52,7 @@
<ImageView
android:onClick="@{editClickListener}"
android:visibility="@{viewModel.isNativeContact ? View.VISIBLE : View.INVISIBLE}"
android:contentDescription="@string/content_description_edit_contact"
android:layout_width="0dp"
android:layout_height="match_parent"
@ -62,6 +63,7 @@
<ImageView
android:onClick="@{deleteClickListener}"
android:visibility="@{viewModel.isNativeContact ? View.VISIBLE : View.INVISIBLE}"
android:contentDescription="@string/content_description_delete_contact"
android:layout_width="0dp"
android:layout_height="match_parent"

View file

@ -62,7 +62,7 @@
<CheckBox
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"
android:visibility="@{selectionListViewModel.isEditionEnabled ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{selectionListViewModel.isEditionEnabled &amp;&amp; viewModel.isNativeContact ? View.VISIBLE : View.GONE, default=gone}"
android:checked="@{selectionListViewModel.selectedItems.contains(position)}"
android:layout_width="wrap_content"
android:layout_height="match_parent"

View file

@ -175,6 +175,10 @@
android:text="@string/no_contact"
android:visibility="@{!viewModel.sipContactsSelected &amp;&amp; viewModel.contactsList.empty ? View.VISIBLE : View.GONE}" />
<include
layout="@layout/wait_layout"
app:visibility="@{viewModel.fetchInProgress}"/>
</RelativeLayout>
<View

View file

@ -107,6 +107,35 @@
linphone:checked="@={viewModel.launcherShortcuts}"
linphone:enabled="@{viewModel.readContactsPermissionGranted}"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{viewModel.ldapAvailable ? View.VISIBLE : View.GONE}"
android:orientation="vertical">
<TextView
style="@style/settings_category_font"
android:text="@string/contacts_settings_ldap_title"
android:paddingTop="15dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
linphone:entries="@{viewModel.ldapConfigurations}"
linphone:layout="@{@layout/settings_ldap_cell}"/>
<include
layout="@layout/settings_widget_basic"
linphone:listener="@{viewModel.ldapNewSettingsListener}"
linphone:title="@{@string/contacts_settings_create_new_ldap_config_title}" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModel" />
</data>
<RelativeLayout
android:onClick="@{() -> data.ldapSettingsListener.onAccountClicked(data.index)}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|left">
<org.linphone.views.MarqueeTextView
android:id="@+id/settings_title"
style="@style/settings_item_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="15dp"
android:layout_marginRight="10dp"
android:singleLine="true"
android:text="@{data.ldapServer, default=`ldap://ldap.example.org`}" />
<View
android:layout_marginTop="15dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/settings_title"
android:background="?attr/dividerColor" />
</RelativeLayout>
</layout>

View file

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:linphone="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<import type="android.text.InputType"/>
<variable
name="backClickListener"
type="android.view.View.OnClickListener"/>
<variable
name="viewModel"
type="org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModel"/>
<variable
name="sharedMainViewModel"
type="org.linphone.activities.main.viewmodels.SharedMainViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_gravity="center_horizontal"
android:background="?attr/lightToolbarBackgroundColor"
android:orientation="horizontal">
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
android:visibility="@{sharedMainViewModel.isSlidingPaneSlideable ? View.VISIBLE : View.GONE}"
android:contentDescription="@string/content_description_go_back"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.2"
android:background="?attr/button_background_drawable"
android:padding="18dp"
android:src="@drawable/back" />
<TextView
style="@style/accent_colored_title_font"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.6"
android:gravity="center"
android:singleLine="true"
android:ellipsize="end"
android:padding="15dp"
android:text="@string/contacts_settings_ldap_title"/>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.2"
android:visibility="@{sharedMainViewModel.isSlidingPaneSlideable ? View.INVISIBLE : View.GONE}" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/contacts_settings_ldap_enable_title}"
linphone:listener="@{viewModel.ldapEnableListener}"
linphone:checked="@={viewModel.ldapEnable}"/>
<include
layout="@layout/settings_widget_basic"
linphone:title="@{@string/contacts_settings_ldap_delete_title}"
linphone:listener="@{viewModel.deleteListener}"/>
<TextView
style="@style/settings_category_font"
android:text="@string/contacts_settings_ldap_connection_title"
android:paddingTop="5dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_server_title}"
linphone:listener="@{viewModel.ldapServerListener}"
linphone:defaultValue="@{viewModel.ldapServer}"
linphone:inputType="@{InputType.TYPE_TEXT_VARIATION_URI}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_bind_dn_title}"
linphone:listener="@{viewModel.ldapBindDnListener}"
linphone:defaultValue="@{viewModel.ldapBindDn}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_password_title}"
linphone:listener="@{viewModel.ldapPasswordListener}"
linphone:defaultValue="@{viewModel.ldapPassword}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD}"/>
<include
layout="@layout/settings_widget_list"
linphone:title="@{@string/contacts_settings_ldap_auth_method_title}"
linphone:listener="@{viewModel.ldapAuthMethodListener}"
linphone:selectedIndex="@{viewModel.ldapAuthMethodIndex}"
linphone:labels="@{viewModel.ldapAuthMethodLabels}"/>
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/contacts_settings_ldap_tls_title}"
linphone:listener="@{viewModel.ldapTlsListener}"
linphone:checked="@={viewModel.ldapTls}"/>
<include
layout="@layout/settings_widget_list"
linphone:title="@{@string/contacts_settings_ldap_cert_check_title}"
linphone:listener="@{viewModel.ldapCertCheckListener}"
linphone:selectedIndex="@{viewModel.ldapCertCheckIndex}"
linphone:labels="@{viewModel.ldapCertCheckLabels}"/>
<TextView
style="@style/settings_category_font"
android:text="@string/contacts_settings_ldap_search_title"
android:paddingTop="5dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_search_base_title}"
linphone:subtitle="@{@string/contacts_settings_ldap_search_base_subtitle}"
linphone:listener="@{viewModel.ldapSearchBaseListener}"
linphone:defaultValue="@{viewModel.ldapSearchBase}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_search_filter_title}"
linphone:listener="@{viewModel.ldapSearchFilterListener}"
linphone:defaultValue="@{viewModel.ldapSearchFilter}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_search_max_results_title}"
linphone:listener="@{viewModel.ldapSearchMaxResultsListener}"
linphone:defaultValue="@{viewModel.ldapSearchMaxResults.toString()}"
linphone:inputType="@{InputType.TYPE_CLASS_NUMBER}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_search_timeout_title}"
linphone:subtitle="@{@string/contacts_settings_ldap_search_timeout_subtitle}"
linphone:listener="@{viewModel.ldapSearchTimeoutListener}"
linphone:defaultValue="@{viewModel.ldapSearchTimeout.toString()}"
linphone:inputType="@{InputType.TYPE_CLASS_NUMBER}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_request_delay_title}"
linphone:subtitle="@{@string/contacts_settings_ldap_request_delay_subtitle}"
linphone:listener="@{viewModel.ldapRequestDelayListener}"
linphone:defaultValue="@{viewModel.ldapRequestDelay.toString()}"
linphone:inputType="@{InputType.TYPE_CLASS_NUMBER}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_minimum_characters_title}"
linphone:listener="@{viewModel.ldapMinimumCharactersListener}"
linphone:defaultValue="@{viewModel.ldapMinimumCharacters.toString()}"
linphone:inputType="@{InputType.TYPE_CLASS_NUMBER}"/>
<TextView
style="@style/settings_category_font"
android:text="@string/contacts_settings_ldap_parsing_title"
android:paddingTop="5dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_name_attribute_title}"
linphone:listener="@{viewModel.ldapNameAttributeListener}"
linphone:defaultValue="@{viewModel.ldapNameAttribute}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_sip_attribute_title}"
linphone:listener="@{viewModel.ldapSipAttributeListener}"
linphone:defaultValue="@{viewModel.ldapSipAttribute}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<include
layout="@layout/settings_widget_text"
linphone:title="@{@string/contacts_settings_ldap_sip_domain_title}"
linphone:listener="@{viewModel.ldapSipDomainListener}"
linphone:defaultValue="@{viewModel.ldapSipDomain}"
linphone:inputType="@{InputType.TYPE_CLASS_TEXT}"/>
<TextView
style="@style/settings_category_font"
android:text="@string/contacts_settings_ldap_misc_title"
android:paddingTop="5dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<include
layout="@layout/settings_widget_switch"
linphone:title="@{@string/contacts_settings_ldap_debug_title}"
linphone:listener="@{viewModel.ldapDebugListener}"
linphone:checked="@={viewModel.ldapDebug}"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>
</layout>

View file

@ -49,7 +49,11 @@
android:id="@+id/contactsSettingsFragment"
android:name="org.linphone.activities.main.settings.fragments.ContactsSettingsFragment"
tools:layout="@layout/settings_contacts_fragment"
android:label="ContactsSettingsFragment" />
android:label="ContactsSettingsFragment" >
<action
android:id="@+id/action_contactsSettingsFragment_to_ldapSettingsFragment"
app:destination="@id/ldapSettingsFragment" />
</fragment>
<fragment
android:id="@+id/networkSettingsFragment"
android:name="org.linphone.activities.main.settings.fragments.NetworkSettingsFragment"
@ -136,6 +140,11 @@
android:name="IsLinking"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/ldapSettingsFragment"
android:name="org.linphone.activities.main.settings.fragments.LdapSettingsFragment"
tools:layout="@layout/settings_ldap_fragment"
android:label="LdapSettingsFragment" />
<fragment
android:id="@+id/conferencesSettingsFragment"
tools:layout="@layout/settings_conferences_fragment"

View file

@ -589,4 +589,38 @@
<string name="about_weblate_translation">Contribuer aux traductions</string>
<string name="assistant_generic_account_warning">Certaines fonctionnalités avancées comme les messages de groupe ou les messages éphémères nécessitent un compte &appName;.\n\nElles seront masquées dans l\'application si vous configurez un compte SIP tiers.\n\nSi vous souhaitez les activer pour un projet professionnel, contactez-nous.</string>
<string name="assistant_generic_account_warning_continue_button_text">J\'ai compris</string>
<string name="contacts_settings_create_new_ldap_config_title">Nouvelle configuration LDAP</string>
<string name="contacts_settings_ldap_title">LDAP</string>
<string name="contacts_settings_ldap_enable_title">Activer</string>
<string name="contacts_settings_ldap_delete_title">Supprimer</string>
<string name="contacts_settings_ldap_connection_title">Connexion</string>
<string name="contacts_settings_ldap_server_title">URL du serveur</string>
<string name="contacts_settings_ldap_bind_dn_title">Bind DN</string>
<string name="contacts_settings_ldap_password_title">Mot de passe</string>
<string name="contacts_settings_ldap_auth_method_title">Méthode d\'authentification</string>
<string name="contacts_settings_ldap_auth_method_anonymous">Anonyme</string>
<string name="contacts_settings_ldap_auth_method_simple">Simple</string>
<string name="contacts_settings_ldap_tls_title">Utiliser TLS</string>
<string name="contacts_settings_ldap_cert_check_title">Vérifier les certificats</string>
<string name="contacts_settings_ldap_cert_check_auto">Auto</string>
<string name="contacts_settings_ldap_cert_check_disabled">Désactivé</string>
<string name="contacts_settings_ldap_cert_check_enabled">Activé</string>
<string name="contacts_settings_ldap_search_title">Recherche</string>
<string name="contacts_settings_ldap_search_base_title">Base de recherche</string>
<string name="contacts_settings_ldap_search_base_subtitle">Ne doit pas être vide !</string>
<string name="contacts_settings_ldap_search_filter_title">Filtre</string>
<string name="contacts_settings_ldap_search_max_results_title">Résultats maximum</string>
<string name="contacts_settings_ldap_search_timeout_title">Durée max</string>
<string name="contacts_settings_ldap_search_timeout_subtitle">En secondes</string>
<string name="contacts_settings_ldap_request_delay_title">Délai entre 2 requêtes</string>
<string name="contacts_settings_ldap_request_delay_subtitle">En millisecondes</string>
<string name="contacts_settings_ldap_minimum_characters_title">Nombre minimum de caractères pour lancer la requête</string>
<string name="contacts_settings_ldap_parsing_title">Analyse</string>
<string name="contacts_settings_ldap_name_attribute_title">Attributs de nom</string>
<string name="contacts_settings_ldap_sip_attribute_title">Attributs SIP</string>
<string name="contacts_settings_ldap_sip_domain_title">Domaine</string>
<string name="contacts_settings_ldap_misc_title">Divers</string>
<string name="contacts_settings_ldap_debug_title">Débogage</string>
<string name="contact_cant_be_deleted">Ce contact ne peut être supprimé</string>
<string name="contacts_ldap_query_more_results_available">Plus de résultats sont disponibles, affinez votre recherche</string>
</resources>

View file

@ -111,6 +111,8 @@
</plurals>
<string name="contact_new_choose_sync_account">Choose where to save the contact</string>
<string name="contact_local_sync_account">Store locally</string>
<string name="contact_cant_be_deleted">This contact can\'t be deleted</string>
<string name="contacts_ldap_query_more_results_available">More results are available, refine your search</string>
<!-- Dialer -->
<string name="dialer_address_bar_hint">Enter a number or an address</string>
@ -543,6 +545,40 @@
<string name="contacts_settings_launcher_shortcuts_summary">Will replace chat room shortcuts if any</string>
<string name="contacts_settings_show_new_contact_account_dialog_title">Always ask in which account save newly created contact</string>
<string name="contacts_settings_show_new_contact_account_dialog_summary">If disabled contact will be stored locally</string>
<string name="contacts_settings_create_new_ldap_config_title">New LDAP configuration</string>
<!-- LDAP Contacts settings -->
<string name="contacts_settings_ldap_title">LDAP</string>
<string name="contacts_settings_ldap_enable_title">Enable</string>
<string name="contacts_settings_ldap_delete_title">Delete</string>
<string name="contacts_settings_ldap_connection_title">Connection</string>
<string name="contacts_settings_ldap_server_title">Server URL</string>
<string name="contacts_settings_ldap_bind_dn_title">Bind DN</string>
<string name="contacts_settings_ldap_password_title">Password</string>
<string name="contacts_settings_ldap_auth_method_title">Authentication method</string>
<string name="contacts_settings_ldap_auth_method_anonymous">Anonymous</string>
<string name="contacts_settings_ldap_auth_method_simple">Simple</string>
<string name="contacts_settings_ldap_tls_title">Use TLS</string>
<string name="contacts_settings_ldap_cert_check_title">Check certificates</string>
<string name="contacts_settings_ldap_cert_check_auto">Auto</string>
<string name="contacts_settings_ldap_cert_check_disabled">Disabled</string>
<string name="contacts_settings_ldap_cert_check_enabled">Enabled</string>
<string name="contacts_settings_ldap_search_title">Search</string>
<string name="contacts_settings_ldap_search_base_title">Search base</string>
<string name="contacts_settings_ldap_search_base_subtitle">Must not be empty!</string>
<string name="contacts_settings_ldap_search_filter_title">Filter</string>
<string name="contacts_settings_ldap_search_max_results_title">Max results</string>
<string name="contacts_settings_ldap_search_timeout_title">Timeout</string>
<string name="contacts_settings_ldap_search_timeout_subtitle">In seconds</string>
<string name="contacts_settings_ldap_request_delay_title">Delay between two requests</string>
<string name="contacts_settings_ldap_request_delay_subtitle">In milliseconds</string>
<string name="contacts_settings_ldap_minimum_characters_title">Minimum characters to start query</string>
<string name="contacts_settings_ldap_parsing_title">Parsing</string>
<string name="contacts_settings_ldap_name_attribute_title">Name attributes</string>
<string name="contacts_settings_ldap_sip_attribute_title">SIP attributes</string>
<string name="contacts_settings_ldap_sip_domain_title">Domain</string>
<string name="contacts_settings_ldap_misc_title">Misc</string>
<string name="contacts_settings_ldap_debug_title">Debug</string>
<!-- Advanced settings -->
<string name="advanced_settings_debug_mode_title">Debug logs</string>