Reworked native address book integration, removed Contact & NativeContact objects to directly rely on Friend

This commit is contained in:
Sylvain Berfini 2022-04-05 16:02:35 +02:00
parent f83eb5e6b1
commit 341c112d54
67 changed files with 838 additions and 1346 deletions

View file

@ -49,7 +49,6 @@ import org.linphone.activities.main.settings.fragments.*
import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment
import org.linphone.activities.voip.CallActivity
import org.linphone.activities.voip.fragments.*
import org.linphone.contact.NativeContact
import org.linphone.core.Address
internal fun Fragment.findMasterNavController(): NavController {
@ -534,9 +533,9 @@ internal fun MasterContactsFragment.clearDisplayedContact() {
}
}
internal fun ContactEditorFragment.navigateToContact(contact: NativeContact) {
internal fun ContactEditorFragment.navigateToContact(id: String) {
val bundle = Bundle()
bundle.putString("id", contact.nativeId)
bundle.putString("id", id)
findNavController().navigate(
R.id.action_contactEditorFragment_to_detailContactFragment,
bundle,
@ -653,8 +652,8 @@ internal fun DetailCallLogFragment.navigateToContacts(sipUriToAdd: String) {
findMasterNavController().navigate(Uri.parse(deepLink))
}
internal fun DetailCallLogFragment.navigateToContact(contact: NativeContact) {
val deepLink = "linphone-android://contact/view/${contact.nativeId}"
internal fun DetailCallLogFragment.navigateToContact(id: String) {
val deepLink = "linphone-android://contact/view/$id"
findMasterNavController().navigate(Uri.parse(deepLink))
}

View file

@ -58,7 +58,7 @@ class EventData(private val eventLog: EventLog) : GenericContactData(
}
private fun getName(): String {
return contact.value?.fullName ?: displayName.value ?: ""
return contact.value?.name ?: displayName.value ?: ""
}
private fun updateEventText() {

View file

@ -166,16 +166,6 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
}
}
override fun goBack() {
if (!findNavController().popBackStack()) {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
navigateToEmptyChatRoom()
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
@ -185,14 +175,23 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Chat Room Creation] READ_CONTACTS permission granted")
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
coreContext.fetchContacts()
} else {
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
}
}
}
override fun goBack() {
if (!findNavController().popBackStack()) {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
navigateToEmptyChatRoom()
}
}
}
private fun addParticipantsFromSharedViewModel() {
val participants = sharedViewModel.chatRoomParticipants.value
if (participants != null && participants.size > 0) {

View file

@ -30,7 +30,6 @@ import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.contact.ContactDataInterface
import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.core.*
@ -49,7 +48,7 @@ class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
}
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
override val showGroupChatAvatar: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) &&
@ -290,7 +289,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
}
val sender: String =
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.name
?: LinphoneUtils.getDisplayName(msg.fromAddress)
builder.append(sender)
builder.append(": ")
@ -332,9 +331,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
var participantsList = ""
var index = 0
for (participant in chatRoom.participants) {
val contact: Contact? =
coreContext.contactsManager.findContactByAddress(participant.address)
participantsList += contact?.fullName ?: LinphoneUtils.getDisplayName(participant.address)
val contact = coreContext.contactsManager.findContactByAddress(participant.address)
participantsList += contact?.name ?: LinphoneUtils.getDisplayName(participant.address)
index++
if (index != chatRoom.nbParticipants) participantsList += ", "
}
@ -361,9 +359,9 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
var composing = ""
for (address in chatRoom.composingAddresses) {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(address)
val contact = coreContext.contactsManager.findContactByAddress(address)
composing += if (composing.isNotEmpty()) ", " else ""
composing += contact?.fullName ?: LinphoneUtils.getDisplayName(address)
composing += contact?.name ?: LinphoneUtils.getDisplayName(address)
}
composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing)
}

View file

@ -68,7 +68,7 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo) {
val contact = coreContext.contactsManager.findContactByAddress(organizerAddress)
organizer.value = if (contact != null)
contact.fullName
contact.name
else
LinphoneUtils.getDisplayName(conferenceInfo.organizer)
} else {
@ -96,7 +96,7 @@ class ScheduledConferenceData(val conferenceInfo: ConferenceInfo) {
for (participant in conferenceInfo.participants) {
val contact = coreContext.contactsManager.findContactByAddress(participant)
val name = if (contact != null) contact.fullName else LinphoneUtils.getDisplayName(participant)
val name = if (contact != null) contact.name else LinphoneUtils.getDisplayName(participant)
val address = participant.asStringUriOnly()
participantsListShort += "$name, "
participantsListExpanded += "$name ($address)\n"

View file

@ -111,8 +111,7 @@ class ConferenceSchedulingParticipantsListFragment : GenericFragment<ConferenceS
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Conference Creation] READ_CONTACTS permission granted")
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
coreContext.fetchContacts()
} else {
Log.w("[Conference Creation] READ_CONTACTS permission denied")
}

View file

@ -32,7 +32,7 @@ import org.linphone.R
import org.linphone.activities.main.adapters.SelectionListAdapter
import org.linphone.activities.main.contact.viewmodels.ContactViewModel
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.contact.Contact
import org.linphone.core.Friend
import org.linphone.databinding.ContactListCellBinding
import org.linphone.databinding.GenericListHeaderBinding
import org.linphone.utils.AppUtils
@ -43,8 +43,8 @@ class ContactsListAdapter(
selectionVM: ListTopBarViewModel,
private val viewLifecycleOwner: LifecycleOwner
) : SelectionListAdapter<ContactViewModel, RecyclerView.ViewHolder>(selectionVM, ContactDiffCallback()), HeaderAdapter {
val selectedContactEvent: MutableLiveData<Event<Contact>> by lazy {
MutableLiveData<Event<Contact>>()
val selectedContactEvent: MutableLiveData<Event<Friend>> by lazy {
MutableLiveData<Event<Friend>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -80,7 +80,9 @@ class ContactsListAdapter(
if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else {
selectedContactEvent.value = Event(contactViewModel.contactInternal)
val friend = contactViewModel.contact.value
// TODO FIXME !!!
if (friend != null) selectedContactEvent.value = Event(friend)
}
}
@ -101,17 +103,17 @@ class ContactsListAdapter(
override fun displayHeaderForPosition(position: Int): Boolean {
if (position >= itemCount) return false
val contact = getItem(position)
val firstLetter = contact.name.first().toString()
val firstLetter = contact.fullName.firstOrNull().toString()
val previousPosition = position - 1
return if (previousPosition >= 0) {
val previousItemFirstLetter = getItem(previousPosition).name.first().toString()
val previousItemFirstLetter = getItem(previousPosition).fullName.firstOrNull().toString()
!firstLetter.equals(previousItemFirstLetter, ignoreCase = true)
} else true
}
override fun getHeaderViewForPosition(context: Context, position: Int): View {
val contact = getItem(position)
val firstLetter = AppUtils.getInitials(contact.name, 1)
val firstLetter = AppUtils.getInitials(contact.fullName, 1)
val binding: GenericListHeaderBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.generic_list_header, null, false
@ -127,7 +129,7 @@ private class ContactDiffCallback : DiffUtil.ItemCallback<ContactViewModel>() {
oldItem: ContactViewModel,
newItem: ContactViewModel
): Boolean {
return oldItem.contactInternal.compareTo(newItem.contactInternal) == 0
return oldItem.fullName.compareTo(newItem.fullName) == 0
}
override fun areContentsTheSame(

View file

@ -41,7 +41,6 @@ import org.linphone.activities.main.contact.viewmodels.*
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToContact
import org.linphone.activities.navigateToEmptyContact
import org.linphone.contact.NativeContact
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactEditorFragmentBinding
import org.linphone.utils.Event
@ -157,10 +156,10 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
private fun saveContact() {
val savedContact = viewModel.save()
if (savedContact is NativeContact) {
savedContact.syncValuesFromAndroidContact(requireContext())
val id = savedContact.refKey
if (id != null) {
Log.i("[Contact Editor] Displaying contact $savedContact")
navigateToContact(savedContact)
navigateToContact(id)
} else {
goBack()
}

View file

@ -70,7 +70,7 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
val contact = sharedViewModel.selectedContact.value
if (contact == null) {
Log.e("[Contact] Contact is null, aborting!")
Log.e("[Contact] Friend is null, aborting!")
goBack()
return
}
@ -146,6 +146,7 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
(activity as MainActivity).showSnackBar(messageResourceId)
}
}
viewModel.updateNumbersAndAddresses()
view.doOnPreDraw {
// Notifies fragment is ready to be drawn
@ -153,6 +154,22 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
}
}
override fun onResume() {
super.onResume()
if (this::viewModel.isInitialized) {
viewModel.registerContactListener()
coreContext.contactsManager.contactIdToWatchFor = viewModel.contact.value?.refKey ?: ""
}
}
override fun onPause() {
super.onPause()
coreContext.contactsManager.contactIdToWatchFor = ""
if (this::viewModel.isInitialized) {
viewModel.unregisterContactListener()
}
}
override fun goBack() {
if (!findNavController().popBackStack()) {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {

View file

@ -46,8 +46,8 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToContact
import org.linphone.activities.navigateToContactEditor
import org.linphone.contact.Contact
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactMasterFragmentBinding
import org.linphone.utils.*
@ -188,13 +188,15 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
viewModel.showDeleteButton(
{
val deletedContact = adapter.currentList[viewHolder.bindingAdapterPosition].contactInternal
listViewModel.deleteContact(deletedContact)
if (!binding.slidingPane.isSlideable &&
deletedContact == sharedViewModel.selectedContact.value
) {
Log.i("[Contacts] Currently displayed contact has been deleted, removing detail fragment")
clearDisplayedContact()
val deletedContact = adapter.currentList[viewHolder.bindingAdapterPosition].contact.value
if (deletedContact != null) {
listViewModel.deleteContact(deletedContact)
if (!binding.slidingPane.isSlideable &&
deletedContact == sharedViewModel.selectedContact.value
) {
Log.i("[Contacts] Currently displayed contact has been deleted, removing detail fragment")
clearDisplayedContact()
}
}
dialog.dismiss()
},
@ -218,7 +220,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
viewLifecycleOwner
) {
it.consume { contact ->
Log.i("[Contacts] Selected item in list changed: $contact")
Log.d("[Contacts] Selected item in list changed: $contact")
sharedViewModel.selectedContact.value = contact
(requireActivity() as MainActivity).hideKeyboard()
@ -232,6 +234,12 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
}
}
coreContext.contactsManager.fetchInProgress.observe(
viewLifecycleOwner
) {
listViewModel.fetchInProgress.value = it
}
listViewModel.contactsList.observe(
viewLifecycleOwner
) {
@ -320,11 +328,13 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
}
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
val list = ArrayList<Contact>()
val list = ArrayList<Friend>()
var closeSlidingPane = false
for (index in indexesOfItemToDelete) {
val contact = adapter.currentList[index].contactInternal
list.add(contact)
val contact = adapter.currentList[index].contact.value
if (contact != null) {
list.add(contact)
}
if (contact == sharedViewModel.selectedContact.value) {
closeSlidingPane = true
@ -347,8 +357,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Contacts] READ_CONTACTS permission granted")
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
coreContext.fetchContacts()
} else {
Log.w("[Contacts] READ_CONTACTS permission denied")
}

View file

@ -32,21 +32,22 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
import org.linphone.contact.*
import org.linphone.core.ChatRoomSecurityLevel
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.utils.ImageUtils
import org.linphone.utils.PermissionHelper
class ContactEditorViewModelFactory(private val contact: Contact?) :
class ContactEditorViewModelFactory(private val friend: Friend?) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContactEditorViewModel(contact) as T
return ContactEditorViewModel(friend) as T
}
}
class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
@ -71,30 +72,37 @@ class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterfac
init {
if (c != null) {
contact.value = c!!
displayName.value = c.fullName ?: c.firstName + " " + c.lastName
displayName.value = c.name ?: ""
} else {
displayName.value = ""
}
firstName.value = c?.firstName ?: ""
lastName.value = c?.lastName ?: ""
organization.value = c?.organization ?: ""
firstName.value = c?.vcard?.givenName ?: ""
lastName.value = c?.vcard?.familyName ?: ""
organization.value = c?.vcard?.organization ?: ""
updateNumbersAndAddresses()
}
fun save(): Contact {
fun save(): Friend {
var contact = c
var created = false
if (contact == null) {
created = true
contact = if (PermissionHelper.get().hasWriteContactsPermission()) {
NativeContact(NativeContactEditor.createAndroidContact(syncAccountName, syncAccountType).toString())
val nativeId = if (PermissionHelper.get().hasWriteContactsPermission()) {
Log.i("[Contact Editor] Creating native contact")
NativeContactEditor.createAndroidContact(syncAccountName, syncAccountType)
.toString()
} else {
Contact()
Log.e("[Contact Editor] Can't native contact, permission denied")
null
}
contact = coreContext.core.createFriend()
contact.refKey = nativeId
}
if (contact is NativeContact) {
if (contact.refKey != null) {
Log.i("[Contact Editor] Committing changes in native contact id ${contact.refKey}")
NativeContactEditor(contact)
.setFirstAndLastNames(firstName.value.orEmpty(), lastName.value.orEmpty())
.setOrganization(organization.value.orEmpty())
@ -102,45 +110,44 @@ class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterfac
.setSipAddresses(addresses.value.orEmpty())
.setPicture(picture)
.commit()
} else {
val friend = contact.friend ?: coreContext.core.createFriend()
friend.edit()
friend.name = "${firstName.value.orEmpty()} ${lastName.value.orEmpty()}"
}
for (address in friend.addresses) {
friend.removeAddress(address)
}
for (address in addresses.value.orEmpty()) {
val parsed = coreContext.core.interpretUrl(address.newValue.value.orEmpty())
if (parsed != null) friend.addAddress(parsed)
}
if (!created) contact.edit()
for (phone in friend.phoneNumbers) {
friend.removePhoneNumber(phone)
}
for (phone in numbers.value.orEmpty()) {
val phoneNumber = phone.newValue.value
if (phoneNumber?.isNotEmpty() == true) {
friend.addPhoneNumber(phoneNumber)
}
}
contact.name = "${firstName.value.orEmpty()} ${lastName.value.orEmpty()}"
contact.organization = organization.value
val vCard = friend.vcard
if (vCard != null) {
vCard.organization = organization.value
vCard.familyName = lastName.value
vCard.givenName = firstName.value
}
friend.done()
for (address in contact.addresses) {
contact.removeAddress(address)
}
for (address in addresses.value.orEmpty()) {
val sipAddress = address.newValue.value.orEmpty()
if (sipAddress.isEmpty()) continue
if (contact.friend == null) {
contact.friend = friend
coreContext.core.defaultFriendList?.addLocalFriend(friend)
}
val parsed = coreContext.core.interpretUrl(sipAddress)
if (parsed != null) contact.addAddress(parsed)
}
for (phone in contact.phoneNumbers) {
contact.removePhoneNumber(phone)
}
for (phone in numbers.value.orEmpty()) {
val phoneNumber = phone.newValue.value.orEmpty()
if (phoneNumber.isEmpty()) continue
contact.addPhoneNumber(phoneNumber)
}
val vCard = contact.vcard
if (vCard != null) {
vCard.familyName = lastName.value
vCard.givenName = firstName.value
}
if (created) {
coreContext.contactsManager.addContact(contact)
coreContext.core.defaultFriendList?.addLocalFriend(contact)
} else {
contact.done()
}
return contact
}
@ -201,8 +208,8 @@ class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterfac
private fun updateNumbersAndAddresses() {
val phoneNumbers = arrayListOf<NumberOrAddressEditorData>()
for (number in c?.rawPhoneNumbers.orEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData(number, false))
for (number in c?.phoneNumbersWithLabel.orEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData(number.phoneNumber, false))
}
if (phoneNumbers.isEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData("", false))
@ -210,8 +217,8 @@ class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterfac
numbers.value = phoneNumbers
val sipAddresses = arrayListOf<NumberOrAddressEditorData>()
for (address in c?.rawSipAddresses.orEmpty()) {
sipAddresses.add(NumberOrAddressEditorData(address, true))
for (address in c?.addresses.orEmpty()) {
sipAddresses.add(NumberOrAddressEditorData(address.asStringUriOnly(), true))
}
if (sipAddresses.isEmpty()) {
sipAddresses.add(NumberOrAddressEditorData("", true))

View file

@ -30,31 +30,29 @@ import org.linphone.R
import org.linphone.activities.main.contact.data.ContactNumberOrAddressClickListener
import org.linphone.activities.main.contact.data.ContactNumberOrAddressData
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.contact.Contact
import org.linphone.contact.ContactDataInterface
import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.contact.NativeContact
import org.linphone.contact.hasPresence
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class ContactViewModelFactory(private val contact: Contact) :
class ContactViewModelFactory(private val friend: Friend) :
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContactViewModel(contact) as T
return ContactViewModel(friend) as T
}
}
class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifierViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
val name: String
get() = displayName.value ?: ""
var fullName = ""
val displayOrganization = corePreferences.displayOrganization
@ -76,28 +74,33 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
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) {
Log.d("[Contact] $contact has changed")
updateNumbersAndAddresses(contact)
}
}
}
private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
if (state == ChatRoom.State.Created) {
chatRoom.removeListener(this)
waitForChatRoomCreation.value = false
chatRoomCreatedEvent.value = Event(chatRoom)
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("[Contact Detail] Group chat room creation has failed !")
chatRoom.removeListener(this)
waitForChatRoomCreation.value = false
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
}
private val contactsListener = object : ContactsUpdatedListenerStub() {
override fun onContactUpdated(friend: Friend) {
if (friend.refKey == contact.value?.refKey) {
Log.i("[Contact Detail] Friend has been updated!")
contact.value = friend
displayName.value = friend.name
isNativeContact.value = friend.refKey != null
updateNumbersAndAddresses()
}
}
}
private val listener = object : ContactNumberOrAddressClickListener {
override fun onCall(address: Address) {
startCallToEvent.value = Event(address)
@ -127,13 +130,17 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
}
init {
contact.value = contactInternal
displayName.value = contactInternal.fullName ?: contactInternal.firstName + " " + contactInternal.lastName
isNativeContact.value = contactInternal is NativeContact
fullName = friend.name ?: ""
updateNumbersAndAddresses(contactInternal)
coreContext.contactsManager.addListener(contactsUpdatedListener)
waitForChatRoomCreation.value = false
if (async) {
contact.postValue(friend)
displayName.postValue(friend.name)
isNativeContact.postValue(friend.refKey != null)
} else {
contact.value = friend
displayName.value = friend.name
isNativeContact.value = friend.refKey != null
}
}
override fun onCleared() {
@ -142,17 +149,24 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
}
fun destroy() {
coreContext.contactsManager.removeListener(contactsUpdatedListener)
}
fun registerContactListener() {
coreContext.contactsManager.addListener(contactsListener)
}
fun unregisterContactListener() {
coreContext.contactsManager.removeListener(contactsListener)
}
fun deleteContact() {
val select = ContactsContract.Data.CONTACT_ID + " = ?"
val ops = java.util.ArrayList<ContentProviderOperation>()
if (contactInternal is NativeContact) {
val nativeContact: NativeContact = contactInternal
Log.i("[Contact] Setting Android contact id ${nativeContact.nativeId} to batch removal")
val args = arrayOf(nativeContact.nativeId)
val id = contact.value?.refKey
if (id != null) {
Log.i("[Contact] Setting Android contact id $id to batch removal")
val args = arrayOf(id)
ops.add(
ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI)
.withSelection(select, args)
@ -160,10 +174,7 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
)
}
if (contactInternal.friend != null) {
Log.i("[Contact] Removing friend")
contactInternal.friend?.remove()
}
contact.value?.remove()
if (ops.isNotEmpty()) {
try {
@ -175,30 +186,37 @@ class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(
}
}
fun updateNumbersAndAddresses(contact: Contact) {
fun updateNumbersAndAddresses() {
val list = arrayListOf<ContactNumberOrAddressData>()
for (address in contact.sipAddresses) {
val friend = contact.value ?: return
for (address in friend.addresses) {
val value = address.asStringUriOnly()
val presenceModel = contact.friend?.getPresenceModelForUriOrTel(value)
val presenceModel = friend.getPresenceModelForUriOrTel(value)
val hasPresence = presenceModel?.basicStatus == PresenceBasicStatus.Open
val isMe = coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual(address) ?: false
val secureChatAllowed = !isMe && contact.friend?.getPresenceModelForUriOrTel(value)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val secureChatAllowed = !isMe && friend.getPresenceModelForUriOrTel(value)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val displayValue = if (coreContext.core.defaultAccount?.params?.domain == address.domain) (address.username ?: value) else value
val noa = ContactNumberOrAddressData(address, hasPresence, displayValue, showSecureChat = secureChatAllowed, listener = listener)
list.add(noa)
}
for (phoneNumber in contact.phoneNumbers) {
val number = phoneNumber.value
val presenceModel = contact.friend?.getPresenceModelForUriOrTel(number)
for (phoneNumber in friend.phoneNumbersWithLabel) {
val number = phoneNumber.phoneNumber
val presenceModel = friend.getPresenceModelForUriOrTel(number)
val hasPresence = presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open
val contactAddress = presenceModel?.contact ?: number
val address = coreContext.core.interpretUrl(contactAddress)
address?.displayName = name
address?.displayName = displayName.value.orEmpty()
val isMe = if (address != null) coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual(address) ?: false else false
val secureChatAllowed = !isMe && contact.friend?.getPresenceModelForUriOrTel(number)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val noa = ContactNumberOrAddressData(address, hasPresence, number, isSip = false, showSecureChat = secureChatAllowed, typeLabel = phoneNumber.typeLabel, listener = listener)
val secureChatAllowed = !isMe && friend.getPresenceModelForUriOrTel(number)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val noa = ContactNumberOrAddressData(address, hasPresence, number, isSip = false, showSecureChat = secureChatAllowed, typeLabel = phoneNumber.label ?: "", listener = listener)
list.add(noa)
}
numbersAndAddresses.value = list
numbersAndAddresses.postValue(list)
}
fun hasPresence(): Boolean {
return contact.value?.hasPresence() ?: false
}
}

View file

@ -25,15 +25,15 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.util.*
import kotlin.collections.HashMap
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
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
import org.linphone.utils.LinphoneUtils
class ContactsListViewModel : ViewModel() {
val sipContactsSelected = MutableLiveData<Boolean>()
@ -60,9 +60,11 @@ class ContactsListViewModel : ViewModel() {
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
Log.i("[Contacts Loader] Magic search contacts available")
searchResultsPending = false
processMagicSearchResults(magicSearch.lastSearch)
fetchInProgress.value = false
// Use coreContext.contactsManager.fetchInProgress instead of false in case contacts are still being loaded
fetchInProgress.value = coreContext.contactsManager.fetchInProgress.value
}
override fun onLdapHaveMoreResults(magicSearch: MagicSearch, ldap: Ldap) {
@ -72,7 +74,6 @@ class ContactsListViewModel : ViewModel() {
init {
sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList()
fetchInProgress.value = false
coreContext.contactsManager.addListener(contactsUpdatedListener)
coreContext.contactsManager.magicSearch.addListener(magicSearchListener)
@ -104,74 +105,81 @@ class ContactsListViewModel : ViewModel() {
val filter = MagicSearchSource.Friends.toInt() or MagicSearchSource.LdapServers.toInt()
searchResultsPending = true
fastFetchJob?.cancel()
Log.i("[Contacts Loader] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$filter]")
coreContext.contactsManager.magicSearch.getContactsAsync(filterValue, domain, filter)
val spinnerDelay = corePreferences.delayBeforeShowingContactsSearchSpinner.toLong()
fastFetchJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
delay(spinnerDelay)
withContext(Dispatchers.Main) {
if (searchResultsPending) {
fetchInProgress.value = true
}
}
withContext(Dispatchers.Main) {
if (searchResultsPending) {
fetchInProgress.value = true
}
}
}
}
private fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("[Contacts] Processing ${results.size} results")
Log.i("[Contacts Loader] Processing ${results.size} results")
contactsList.value.orEmpty().forEach(ContactViewModel::destroy)
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 {
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))
}
viewModelScope.launch {
withContext(Dispatchers.IO) {
val list = arrayListOf<ContactViewModel>()
val viewModels = HashMap<String, ContactViewModel>()
contactsList.postValue(list)
for (result in results) {
val friend = result.friend
val name = friend?.name ?: LinphoneUtils.getDisplayName(result.address)
val found = viewModels[name]
if (found != null && friend != null) {
continue
}
val viewModel = if (friend != null) {
ContactViewModel(friend, true)
} else {
val fakeFriend = coreContext.contactsManager.createFriendFromSearchResult(result)
ContactViewModel(fakeFriend, true)
}
list.add(viewModel)
if (found == null) {
viewModels[name] = viewModel
}
}
contactsList.postValue(list)
viewModels.clear()
}
withContext(Dispatchers.Main) {
Log.i("[Contacts Loader] Processed ${results.size} results")
}
}
}
fun deleteContact(contact: Contact?) {
contact ?: return
fun deleteContact(friend: Friend) {
friend.remove() // TODO: FIXME: friend is const here!
val id = friend.refKey
if (id == null) {
Log.w("[Contacts] Friend has no refkey, can't delete it from native address book")
return
}
val select = ContactsContract.Data.CONTACT_ID + " = ?"
val ops = ArrayList<ContentProviderOperation>()
if (contact is NativeContact) {
val nativeContact: NativeContact = contact
Log.i("[Contacts] Adding Android contact id ${nativeContact.nativeId} to batch removal")
val args = arrayOf(nativeContact.nativeId)
ops.add(
ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI)
.withSelection(select, args)
.build()
)
}
if (contact.friend != null) {
Log.i("[Contacts] Removing friend")
contact.friend?.remove()
}
Log.i("[Contacts] Adding Android contact id $id to batch removal")
val args = arrayOf(id)
ops.add(
ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI)
.withSelection(select, args)
.build()
)
if (ops.isNotEmpty()) {
try {
@ -183,26 +191,22 @@ class ContactsListViewModel : ViewModel() {
}
}
fun deleteContacts(list: ArrayList<Contact>) {
fun deleteContacts(list: ArrayList<Friend>) {
val select = ContactsContract.Data.CONTACT_ID + " = ?"
val ops = ArrayList<ContentProviderOperation>()
for (contact in list) {
if (contact is NativeContact) {
val nativeContact: NativeContact = contact
Log.i("[Contacts] Adding Android contact id ${nativeContact.nativeId} to batch removal")
val args = arrayOf(nativeContact.nativeId)
for (friend in list) {
val id = friend.refKey
if (id != null) {
Log.i("[Contacts] Adding Android contact id $id to batch removal")
val args = arrayOf(id)
ops.add(
ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI)
.withSelection(select, args)
.build()
)
}
if (contact.friend != null) {
Log.i("[Contacts] Removing friend")
contact.friend?.remove()
}
friend.remove()
}
if (ops.isNotEmpty()) {
@ -214,32 +218,4 @@ class ContactsListViewModel : ViewModel() {
}
}
}
private fun searchMatchingContact(searchResult: SearchResult): Contact? {
val friend = searchResult.friend
var displayName = ""
if (friend != null) {
displayName = friend.name ?: ""
val contact: Contact? = friend.userData as? Contact
if (contact != null) return contact
val friendContact = coreContext.contactsManager.findContactByFriend(friend)
if (friendContact != null) return friendContact
}
val address = searchResult.address
if (address != null) {
if (displayName.isEmpty()) displayName = address.displayName ?: ""
val contact = coreContext.contactsManager.findContactByAddress(address, ignoreLocalContact = true)
if (contact != null && (displayName.isEmpty() || contact.fullName == displayName)) return contact
}
val phoneNumber = searchResult.phoneNumber
if (phoneNumber != null && address?.username != phoneNumber) {
val contact = coreContext.contactsManager.findContactByPhoneNumber(phoneNumber)
if (contact != null && (displayName.isEmpty() || contact.fullName == displayName)) return contact
}
return null
}
}

View file

@ -32,7 +32,6 @@ import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToContact
import org.linphone.activities.navigateToContacts
import org.linphone.activities.navigateToFriend
import org.linphone.contact.NativeContact
import org.linphone.core.tools.Log
import org.linphone.databinding.HistoryDetailFragmentBinding
import org.linphone.utils.Event
@ -80,10 +79,10 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
binding.setContactClickListener {
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
val contact = viewModel.contact.value as? NativeContact
if (contact != null) {
Log.i("[History] Displaying contact $contact")
navigateToContact(contact)
val contactId = viewModel.contact.value?.refKey
if (contactId != null) {
Log.i("[History] Displaying contact $contactId")
navigateToContact(contactId)
} else {
val copy = viewModel.callLog.remoteAddress.clone()
copy.clean()

View file

@ -99,7 +99,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
val chatAllowed = !corePreferences.disableChat
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val secureChatAllowed = contact.value?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val relatedCallLogs = MutableLiveData<ArrayList<CallLogViewModel>>()

View file

@ -109,8 +109,7 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
if (granted) {
Log.i("[Contacts Settings] READ_CONTACTS permission granted")
viewModel.readContactsPermissionGranted.value = true
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
coreContext.fetchContacts()
} else {
Log.w("[Contacts Settings] READ_CONTACTS permission denied")
}

View file

@ -23,7 +23,6 @@ import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.activities.main.history.data.GroupedCallLogData
import org.linphone.contact.Contact
import org.linphone.core.*
import org.linphone.utils.Event
@ -76,7 +75,7 @@ class SharedMainViewModel : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val selectedContact = MutableLiveData<Contact>()
val selectedContact = MutableLiveData<Friend>()
// For correct animations directions
val updateContactsAnimationsBasedOnDestination: MutableLiveData<Event<Int>> by lazy {

View file

@ -25,7 +25,7 @@ import android.view.View
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.voip.viewmodels.ConferenceParticipantsViewModel
@ -123,8 +123,7 @@ class ConferenceAddParticipantsFragment : GenericFragment<VoipConferenceParticip
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Conference Add Participants] READ_CONTACTS permission granted")
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
LinphoneApplication.coreContext.fetchContacts()
} else {
Log.w("[Conference Add Participants] READ_CONTACTS permission denied")
}

View file

@ -56,7 +56,7 @@ class ConferenceParticipantsFragment : GenericFragment<VoipConferenceParticipant
) {
it.consume { participantData ->
val participantName =
participantData.contact.value?.fullName ?: participantData.displayName.value
participantData.contact.value?.name ?: participantData.displayName.value
val message = if (participantData.participant.isAdmin) {
getString(R.string.conference_admin_set).format(participantName)
} else {

View file

@ -38,8 +38,9 @@ import androidx.core.content.ContextCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.contact.getThumbnailUri
import org.linphone.core.Call
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.notifications.Notifiable
import org.linphone.notifications.NotificationsManager
@ -144,10 +145,9 @@ class Api26Compatibility {
pendingIntent: PendingIntent,
notificationsManager: NotificationsManager
): Notification {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val contact: Friend? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri())
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val address = LinphoneUtils.getDisplayableAddress(call.remoteAddress)
val notificationLayoutHeadsUp = RemoteViews(context.packageName, R.layout.call_incoming_notification_heads_up)
@ -193,10 +193,9 @@ class Api26Compatibility {
channel: String,
notificationsManager: NotificationsManager
): Notification {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val contact: Friend? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri())
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val stringResourceId: Int
val iconResourceId: Int
@ -226,7 +225,7 @@ class Api26Compatibility {
val builder = NotificationCompat.Builder(
context, channel
)
.setContentTitle(contact?.fullName ?: displayName)
.setContentTitle(contact?.name ?: displayName)
.setContentText(context.getString(stringResourceId))
.setSmallIcon(iconResourceId)
.setLargeIcon(roundPicture)

View file

@ -29,7 +29,7 @@ import androidx.core.content.ContextCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.contact.getThumbnailUri
import org.linphone.core.Call
import org.linphone.core.tools.Log
import org.linphone.notifications.Notifiable
@ -51,10 +51,9 @@ class Api31Compatibility {
pendingIntent: PendingIntent,
notificationsManager: NotificationsManager
): Notification {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri())
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val person = notificationsManager.getPerson(contact, displayName, roundPicture)
val caller = Person.Builder()
@ -99,11 +98,10 @@ class Api31Compatibility {
channel: String,
notificationsManager: NotificationsManager
): Notification {
val contact: Contact? =
val contact =
coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri())
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val isVideo = call.currentParams.isVideoEnabled
val iconResourceId: Int = when (call.state) {

View file

@ -28,7 +28,7 @@ import androidx.core.content.ContextCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.contact.getThumbnailUri
import org.linphone.core.Call
import org.linphone.notifications.Notifiable
import org.linphone.notifications.NotificationsManager
@ -45,10 +45,9 @@ class XiaomiCompatibility {
pendingIntent: PendingIntent,
notificationsManager: NotificationsManager
): Notification {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri())
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val address = LinphoneUtils.getDisplayableAddress(call.remoteAddress)
val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id))

View file

@ -1,260 +0,0 @@
/*
* Copyright (c) 2010-2020 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.contact
import android.content.Context
import android.database.Cursor
import android.os.AsyncTask
import android.provider.ContactsContract
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.PermissionHelper
class AsyncContactsLoader(private val context: Context) :
AsyncTask<Void, Void, AsyncContactsLoader.AsyncContactsData>() {
companion object {
val projection = arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Data.MIMETYPE,
ContactsContract.Contacts.STARRED,
ContactsContract.Contacts.LOOKUP_KEY,
"data1", // Company, Phone or SIP Address
"data2", // ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.SipAddress.TYPE
"data3", // ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, ContactsContract.CommonDataKinds.Phone.LABEL, ContactsContract.CommonDataKinds.SipAddress.LABEL
"data4"
)
}
override fun onPreExecute() {
if (isCancelled) return
Log.i("[Contacts Loader] Synchronization started")
}
override fun doInBackground(vararg args: Void): AsyncContactsData {
val data = AsyncContactsData()
if (isCancelled) return data
Log.i("[Contacts Loader] Background synchronization started")
val core: Core = coreContext.core
val androidContactsCache: HashMap<String, Contact> = HashMap()
val nativeIds = arrayListOf<String>()
val friendLists = core.friendsLists
for (list in friendLists) {
val friends = list.friends
for (friend in friends) {
if (isCancelled) {
Log.w("[Contacts Loader] Task cancelled")
return data
}
var contact: Contact? = friend.userData as? Contact
if (contact != null) {
if (contact is NativeContact) {
contact.sipAddresses.clear()
contact.rawSipAddresses.clear()
contact.phoneNumbers.clear()
contact.rawPhoneNumbers.clear()
androidContactsCache[contact.nativeId] = contact
nativeIds.add(contact.nativeId)
} else {
data.contacts.add(contact)
}
} else {
if (friend.refKey != null) {
// Friend has a refkey but no LinphoneContact => represents a
// native contact stored in db from a previous version of Linphone,
// remove it
list.removeFriend(friend)
} else { // No refkey so it's a standalone contact
contact = Contact()
contact.friend = friend
contact.syncValuesFromFriend()
friend.userData = contact
data.contacts.add(contact)
}
}
}
}
if (PermissionHelper.required(context).hasReadContactsPermission()) {
var selection: String? = null
if (corePreferences.fetchContactsFromDefaultDirectory) {
Log.i("[Contacts Loader] Only fetching contacts in default directory")
selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1"
}
val cursor: Cursor? = try {
context.contentResolver
.query(
ContactsContract.Data.CONTENT_URI,
projection,
selection,
null,
null
)
} catch (e: Exception) {
Log.e("[Contacts Loader] Failed to get contacts cursor: $e")
null
}
if (cursor != null) {
Log.i("[Contacts Loader] Found ${cursor.count} entries in cursor")
while (cursor.moveToNext()) {
if (isCancelled) {
Log.w("[Contacts Loader] Task cancelled")
cursor.close()
return data
}
try {
val id: String =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID))
val starred =
cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)) == 1
val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
var contact: Contact? = androidContactsCache[id]
if (contact == null) {
Log.d(
"[Contacts Loader] Creating contact with native ID $id, favorite flag is $starred"
)
nativeIds.add(id)
contact = NativeContact(id, lookupKey)
contact.isStarred = starred
androidContactsCache[id] = contact
}
contact.syncValuesFromAndroidCursor(cursor)
} catch (ise: IllegalStateException) {
Log.e(
"[Contacts Loader] Couldn't get values from cursor, exception: $ise"
)
} catch (iae: IllegalArgumentException) {
Log.e(
"[Contacts Loader] Couldn't get values from cursor, exception: $iae"
)
}
}
cursor.close()
} else {
Log.w("[Contacts Loader] Read contacts permission denied, can't fetch native contacts")
}
for (list in core.friendsLists) {
val friends = list.friends
for (friend in friends) {
if (isCancelled) {
Log.w("[Contacts Loader] Task cancelled")
return data
}
val contact: Contact? = friend.userData as? Contact
if (contact != null && contact is NativeContact) {
if (!nativeIds.contains(contact.nativeId)) {
Log.i("[Contacts Loader] Contact removed since last fetch: ${contact.nativeId}")
// Has been removed since last fetch
androidContactsCache.remove(contact.nativeId)
}
}
}
}
nativeIds.clear()
}
val contacts: Collection<Contact> = androidContactsCache.values
// New friends count will be 0 after the first contacts fetch
Log.i(
"[Contacts Loader] Found ${contacts.size} native contacts plus ${data.contacts.size} friends in the configuration file"
)
for (contact in contacts) {
if (isCancelled) {
Log.w("[Contacts Loader] Task cancelled")
return data
}
if (contact.sipAddresses.isEmpty() && contact.phoneNumbers.isEmpty()) {
continue
}
if (contact.fullName == null) {
for (address in contact.sipAddresses) {
contact.fullName = LinphoneUtils.getDisplayName(address)
Log.w(
"[Contacts Loader] Couldn't find a display name for contact ${contact.fullName}, used SIP address display name / username instead..."
)
}
}
data.contacts.add(contact)
}
androidContactsCache.clear()
data.contacts.sort()
Log.i("[Contacts Loader] Background synchronization finished")
return data
}
override fun onPostExecute(data: AsyncContactsData) {
if (isCancelled) return
Log.i("[Contacts Loader] ${data.contacts.size} contacts")
for (contact in data.contacts) {
if (contact is NativeContact) {
contact.createOrUpdateFriendFromNativeContact()
}
}
// Now that contact fetching is asynchronous, this is required to ensure
// presence subscription event will be sent with all friends
val core = coreContext.core
if (core.isFriendListSubscriptionEnabled) {
Log.i("[Contacts Loader] Matching friends created, updating subscription")
for (list in core.friendsLists) {
if (list.rlsAddress == null) {
Log.w("[Contacts Loader] Friend list subscription enabled but RLS URI not set!")
val defaultRlsUri = corePreferences.defaultRlsUri
if (defaultRlsUri.isNotEmpty()) {
val rlsAddress = core.interpretUrl(defaultRlsUri)
if (rlsAddress != null) {
Log.i("[Contacts Loader] Using new RLS URI: ${rlsAddress.asStringUriOnly()}")
list.rlsAddress = rlsAddress
} else {
Log.e("[Contacts Loader] Couldn't parse RLS URI: $defaultRlsUri")
}
} else {
Log.e("[Contacts Loader] RLS URI not found in config file!")
}
}
list.updateSubscriptions()
}
}
coreContext.contactsManager.updateContacts(data.contacts)
Log.i("[Contacts Loader] Synchronization finished")
}
class AsyncContactsData {
val contacts = arrayListOf<Contact>()
}
}

View file

@ -58,16 +58,17 @@ class BigContactAvatarView : LinearLayout {
}
binding.root.visibility = View.VISIBLE
val contact: Contact? = viewModel.contact.value
val contact = viewModel.contact.value
val initials = if (contact != null) {
AppUtils.getInitials(contact.fullName ?: contact.firstName + " " + contact.lastName)
AppUtils.getInitials(contact.name ?: "")
} else {
AppUtils.getInitials(viewModel.displayName.value ?: "")
}
binding.initials = initials
binding.generatedAvatarVisibility = initials.isNotEmpty() && initials != "+"
binding.imagePath = contact?.getContactPictureUri()
binding.imagePath = contact?.getPictureUri()
binding.thumbnailPath = contact?.getThumbnailUri()
binding.borderVisibility = corePreferences.showBorderOnBigContactAvatar
}
}

View file

@ -1,216 +0,0 @@
/*
* Copyright (c) 2010-2020 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.contact
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import org.linphone.LinphoneApplication.Companion.coreContext
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 {
return value.compareTo(other.value)
}
}
open class Contact() : Comparable<Contact> {
var fullName: String? = null
var firstName: String? = null
var lastName: String? = null
var organization: String? = null
var isStarred: Boolean = false
var phoneNumbers = arrayListOf<PhoneNumber>()
var rawPhoneNumbers = arrayListOf<String>()
var sipAddresses = arrayListOf<Address>()
// Raw SIP addresses are only used for contact edition
var rawSipAddresses = arrayListOf<String>()
var friend: Friend? = null
private var thumbnailUri: Uri? = null
constructor(searchResult: SearchResult) : this() {
friend = searchResult.friend
fullName = friend?.name
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()
}
if (address != null && address.username == phoneNumber) {
sipAddresses.remove(address)
}
phoneNumbers.add(PhoneNumber(phoneNumber, ""))
}
}
override fun compareTo(other: Contact): Int {
val fn = fullName ?: ""
val otherFn = other.fullName ?: ""
if (fn == otherFn) {
if (phoneNumbers.size == other.phoneNumbers.size && phoneNumbers.size > 0) {
if (phoneNumbers != other.phoneNumbers) {
for (i in 0 until phoneNumbers.size) {
val compare = phoneNumbers[i].compareTo(other.phoneNumbers[i])
if (compare != 0) return compare
}
}
} else {
return phoneNumbers.size.compareTo(other.phoneNumbers.size)
}
if (sipAddresses.size == other.sipAddresses.size && sipAddresses.size > 0) {
if (sipAddresses != other.sipAddresses) {
for (i in 0 until sipAddresses.size) {
val compare = sipAddresses[i].asStringUriOnly().compareTo(other.sipAddresses[i].asStringUriOnly())
if (compare != 0) return compare
}
}
} else {
return sipAddresses.size.compareTo(other.sipAddresses.size)
}
val org = organization ?: ""
val otherOrg = other.organization ?: ""
return org.compareTo(otherOrg)
}
return coreContext.collator.compare(fn, otherFn)
}
@Synchronized
fun syncValuesFromFriend() {
val friend = this.friend
friend ?: return
phoneNumbers.clear()
for (number in friend.phoneNumbers) {
if (!rawPhoneNumbers.contains(number)) {
phoneNumbers.add(PhoneNumber(number, ""))
rawPhoneNumbers.add(number)
}
}
sipAddresses.clear()
rawSipAddresses.clear()
for (address in friend.addresses) {
val stringAddress = address.asStringUriOnly()
if (!rawSipAddresses.contains(stringAddress)) {
sipAddresses.add(address)
rawSipAddresses.add(stringAddress)
}
}
fullName = friend.name
val vCard = friend.vcard
if (vCard != null) {
lastName = vCard.familyName
firstName = vCard.givenName
organization = vCard.organization
}
}
@Synchronized
open fun syncValuesFromAndroidCursor(cursor: Cursor) {
Log.e("[Contact] Not a native contact, skip")
}
open fun getContactThumbnailPictureUri(): Uri? {
return thumbnailUri
}
fun setContactThumbnailPictureUri(uri: Uri) {
thumbnailUri = uri
}
open fun getContactPictureUri(): Uri? {
return thumbnailUri
}
open fun getPerson(): Person {
val personBuilder = Person.Builder().setName(fullName)
val bm: Bitmap? =
ImageUtils.getRoundBitmapFromUri(
coreContext.context,
getContactThumbnailPictureUri()
)
val icon =
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.voip_single_contact_avatar_alt
) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
personBuilder.setImportant(isStarred)
return personBuilder.build()
}
fun hasPresence(): Boolean {
if (friend == null) return false
for (address in sipAddresses) {
val presenceModel = friend?.getPresenceModelForUriOrTel(address.asStringUriOnly())
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
}
for (number in rawPhoneNumbers) {
val presenceModel = friend?.getPresenceModelForUriOrTel(number)
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
}
return false
}
fun getContactForPhoneNumberOrAddress(value: String): String? {
val presenceModel = friend?.getPresenceModelForUriOrTel(value)
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return presenceModel.contact
return null
}
override fun toString(): String {
return "${super.toString()}: name [$fullName]"
}
}

View file

@ -52,9 +52,9 @@ class ContactAvatarView : LinearLayout {
}
fun setData(data: ContactDataInterface) {
val contact: Contact? = data.contact.value
val contact = data.contact.value
val initials = if (contact != null) {
AppUtils.getInitials(contact.fullName ?: contact.firstName + " " + contact.lastName)
AppUtils.getInitials(contact.name ?: "")
} else {
AppUtils.getInitials(data.displayName.value ?: "")
}
@ -63,7 +63,7 @@ class ContactAvatarView : LinearLayout {
binding.generatedAvatarVisibility = initials.isNotEmpty() && initials != "+"
binding.groupChatAvatarVisibility = data.showGroupChatAvatar
binding.imagePath = contact?.getContactThumbnailPictureUri()
binding.imagePath = contact?.getThumbnailUri()
binding.borderVisibility = corePreferences.showBorderOnContactAvatar
binding.securityIcon = when (data.securityLevel.value) {

View file

@ -19,16 +19,18 @@
*/
package org.linphone.contact
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.core.Address
import org.linphone.core.ChatRoomSecurityLevel
import org.linphone.core.Friend
import org.linphone.utils.AppUtils
import org.linphone.utils.LinphoneUtils
interface ContactDataInterface {
val contact: MutableLiveData<Contact>
val contact: MutableLiveData<Friend>
val displayName: MutableLiveData<String>
@ -39,14 +41,21 @@ interface ContactDataInterface {
}
open class GenericContactData(private val sipAddress: Address) : ContactDataInterface {
final override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
final override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
final override val displayName: MutableLiveData<String> = MutableLiveData<String>()
final override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
val initials = MutableLiveData<String>()
val displayInitials = MutableLiveData<Boolean>()
val thumbnailUri: Uri?
get() = contact.value?.getThumbnailUri()
val pictureUri: Uri?
get() = contact.value?.getPictureUri()
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
override fun onContactUpdated(contact: Contact) {
override fun onContactUpdated(friend: Friend) {
contactLookup()
}
}
@ -68,7 +77,7 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte
contact.value = c
initials.value = if (c != null) {
AppUtils.getInitials(c.fullName ?: c.firstName + " " + c.lastName)
AppUtils.getInitials(c.name ?: "")
} else {
AppUtils.getInitials(displayName.value ?: "")
}
@ -77,12 +86,18 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte
}
abstract class GenericContactViewModel(private val sipAddress: Address) : MessageNotifierViewModel(), ContactDataInterface {
final override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
final override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
final override val displayName: MutableLiveData<String> = MutableLiveData<String>()
final override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
val thumbnailUri: Uri?
get() = contact.value?.getThumbnailUri()
val pictureUri: Uri?
get() = contact.value?.getPictureUri()
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
override fun onContactUpdated(contact: Contact) {
override fun onContactUpdated(friend: Friend) {
contactLookup()
}
}

View file

@ -0,0 +1,210 @@
/*
* Copyright (c) 2010-2021 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.contact
import android.content.ContentUris
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import android.util.Patterns
import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import java.lang.Exception
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Factory
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
companion object {
val projection = arrayOf(
ContactsContract.Data.CONTACT_ID,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Data.MIMETYPE,
ContactsContract.Contacts.STARRED,
ContactsContract.Contacts.LOOKUP_KEY,
"data1", // Company, Phone or SIP Address
"data2", // ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.SipAddress.TYPE
"data3", // ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, ContactsContract.CommonDataKinds.Phone.LABEL, ContactsContract.CommonDataKinds.SipAddress.LABEL
"data4"
)
}
private val friends = HashMap<String, Friend>()
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
Log.i("[Contacts Loader] Loader created")
coreContext.contactsManager.fetchInProgress.value = true
return CursorLoader(
coreContext.context,
ContactsContract.Data.CONTENT_URI,
projection,
ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1",
null,
null
)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor) {
Log.i("[Contacts Loader] Load finished, found ${cursor.count} entries in cursor")
friends.clear()
val core = coreContext.core
val linphoneMime = loader.context.getString(R.string.linphone_address_mime_type)
coreContext.lifecycleScope.launch {
withContext(Dispatchers.IO) {
while (!cursor.isClosed && cursor.moveToNext()) {
try {
val id: String =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID))
val displayName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME_PRIMARY))
val mime: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE))
val data1: String? = cursor.getString(cursor.getColumnIndexOrThrow("data1"))
val data2: String? = cursor.getString(cursor.getColumnIndexOrThrow("data2"))
val data3: String? = cursor.getString(cursor.getColumnIndexOrThrow("data3"))
val data4: String? = cursor.getString(cursor.getColumnIndexOrThrow("data4"))
val starred =
cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)) == 1
val lookupKey =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
val friend = friends[id] ?: core.createFriend()
friend.refKey = id
if (friend.name.isNullOrEmpty()) {
friend.name = displayName
friend.photo = Uri.withAppendedPath(
ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI,
id.toLong()
),
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
).toString()
friend.starred = starred
friend.nativeUri =
"${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey"
}
when (mime) {
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
val typeLabel = ContactsContract.CommonDataKinds.Phone.getTypeLabel(
loader.context.resources,
data2?.toInt() ?: 0,
data3
).toString()
val number =
if (corePreferences.preferNormalizedPhoneNumbersFromAddressBook ||
data1.isNullOrEmpty() ||
!Patterns.PHONE.matcher(data1).matches()
) {
data4 ?: data1
} else {
data1
}
if (number != null) {
var duplicate = false
for (pn in friend.phoneNumbersWithLabel) {
if (pn.label == typeLabel && LinphoneUtils.arePhoneNumberWeakEqual(
pn.phoneNumber,
number
)
) {
duplicate = true
break
}
}
if (!duplicate) {
val phoneNumber = Factory.instance()
.createFriendPhoneNumber(number, typeLabel)
friend.addPhoneNumberWithLabel(phoneNumber)
}
}
}
linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
if (data1 == null) continue
val address = core.interpretUrl(data1) ?: continue
friend.addAddress(address)
}
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
if (data1 == null) continue
val vCard = friend.vcard
vCard?.organization = data1
}
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
if (data2 == null && data3 == null) continue
val vCard = friend.vcard
vCard?.givenName = data2
vCard?.familyName = data3
}
}
friends[id] = friend
} catch (e: Exception) {
Log.e("[Contacts Loader] Exception: $e")
}
}
withContext(Dispatchers.Main) {
Log.i("[Contacts Loader] Friends created")
val contactId = coreContext.contactsManager.contactIdToWatchFor
if (contactId.isNotEmpty()) {
val friend = friends[contactId]
Log.i("[Contacts Loader] Manager was asked to monitor contact id $contactId")
if (friend != null) {
Log.i("[Contacts Loader] Found new contact matching id $contactId, notifying listeners")
coreContext.contactsManager.notifyListeners(friend)
}
}
val fl = core.defaultFriendList ?: core.createFriendList()
for (friend in fl.friends) {
fl.removeFriend(friend)
}
if (fl != core.defaultFriendList) core.addFriendList(fl)
for (friend in friends.values) {
fl.addLocalFriend(friend)
}
fl.updateSubscriptions()
Log.i("[Contacts Loader] Friends added & subscription updated")
friends.clear()
coreContext.contactsManager.fetchFinished()
}
}
}
}
override fun onLoaderReset(loader: Loader<Cursor>) {
Log.i("[Contacts Loader] Loader reset")
}
}

View file

@ -25,7 +25,7 @@ import org.linphone.core.*
import org.linphone.utils.LinphoneUtils
class ContactSelectionData(private val searchResult: SearchResult) : ContactDataInterface {
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()

View file

@ -23,57 +23,39 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AuthenticatorDescription
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.ContentObserver
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.AsyncTask
import android.os.AsyncTask.THREAD_POOL_EXECUTOR
import android.provider.ContactsContract
import android.util.Patterns
import java.io.File
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.MutableLiveData
import java.lang.NumberFormatException
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.ImageUtils
import org.linphone.utils.PermissionHelper
interface ContactsUpdatedListener {
fun onContactsUpdated()
fun onContactUpdated(contact: Contact)
fun onContactUpdated(friend: Friend)
}
open class ContactsUpdatedListenerStub : ContactsUpdatedListener {
override fun onContactsUpdated() {}
override fun onContactUpdated(contact: Contact) {}
override fun onContactUpdated(friend: Friend) {}
}
class ContactsManager(private val context: Context) {
private val contactsObserver: ContentObserver by lazy {
object : ContentObserver(coreContext.handler) {
@Synchronized
override fun onChange(selfChange: Boolean) {
onChange(selfChange, null)
}
@Synchronized
override fun onChange(selfChange: Boolean, uri: Uri?) {
Log.i("[Contacts Observer] At least one contact has changed")
fetchContactsAsync()
}
}
}
var contacts = ArrayList<Contact>()
@Synchronized
get
@Synchronized
private set
val magicSearch: MagicSearch by lazy {
val magicSearch = coreContext.core.createMagicSearch()
magicSearch.limitedSearch = false
@ -82,18 +64,14 @@ class ContactsManager(private val context: Context) {
var latestContactFetch: String = ""
private var localAccountsContacts = ArrayList<Contact>()
@Synchronized
get
@Synchronized
private set
val fetchInProgress = MutableLiveData<Boolean>()
private val friendsMap: HashMap<String, Contact> = HashMap()
var contactIdToWatchFor: String = ""
private val localFriends = arrayListOf<Friend>()
private val contactsUpdatedListeners = ArrayList<ContactsUpdatedListener>()
private var loadContactsTask: AsyncContactsLoader? = null
private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() {
@Synchronized
override fun onPresenceReceived(list: FriendList, friends: Array<Friend>) {
@ -106,10 +84,6 @@ class ContactsManager(private val context: Context) {
}
init {
if (PermissionHelper.required(context).hasReadContactsPermission()) {
onReadContactsPermissionGranted()
}
initSyncAccount()
val core = coreContext.core
@ -119,75 +93,42 @@ class ContactsManager(private val context: Context) {
Log.i("[Contacts Manager] Created")
}
fun onReadContactsPermissionGranted() {
Log.i("[Contacts Manager] Register contacts observer")
context.contentResolver.registerContentObserver(
ContactsContract.Contacts.CONTENT_URI,
true,
contactsObserver
)
}
fun shouldDisplaySipContactsList(): Boolean {
return coreContext.core.defaultAccount?.params?.identityAddress?.domain == corePreferences.defaultDomain
}
@Synchronized
fun fetchContactsAsync() {
fun fetchFinished() {
Log.i("[Contacts Manager] Contacts loader have finished")
latestContactFetch = System.currentTimeMillis().toString()
if (loadContactsTask != null) {
Log.w("[Contacts Manager] Cancelling existing async task")
loadContactsTask?.cancel(true)
}
loadContactsTask = AsyncContactsLoader(context)
loadContactsTask?.executeOnExecutor(THREAD_POOL_EXECUTOR)
}
@Synchronized
fun addContact(contact: Contact) {
contacts.add(contact)
updateLocalContacts()
fetchInProgress.value = false
notifyListeners()
}
@Synchronized
fun updateLocalContacts() {
Log.i("[Contacts Manager] Updating local contact(s)")
localAccountsContacts.clear()
localFriends.clear()
for (account in coreContext.core.accountList) {
val localContact = Contact()
localContact.fullName = account.params.identityAddress?.displayName ?: account.params.identityAddress?.username
val friend = coreContext.core.createFriend()
friend.name = account.params.identityAddress?.displayName ?: account.params.identityAddress?.username
val address = account.params.identityAddress ?: continue
friend.address = address
val pictureUri = corePreferences.defaultAccountAvatarPath
if (pictureUri != null) {
Log.i("[Contacts Manager] Found local picture URI: $pictureUri")
localContact.setContactThumbnailPictureUri(Uri.fromFile(File(pictureUri)))
val parsedUri = if (pictureUri.startsWith("/")) "file:$pictureUri" else pictureUri
Log.i("[Contacts Manager] Found local picture URI: $parsedUri")
friend.photo = parsedUri
}
val address = account.params.identityAddress
if (address != null) {
localContact.sipAddresses.add(address)
localContact.rawSipAddresses.add(address.asStringUriOnly())
}
localAccountsContacts.add(localContact)
Log.i("[Contacts Manager] Local contact created for account [${address.asString()}] and picture [${friend.photo}]")
localFriends.add(friend)
}
}
@Synchronized
fun updateContacts(all: ArrayList<Contact>) {
friendsMap.clear()
// Contact has a Friend field and Friend can have a Contact has userData
// Friend also keeps a ref on the Core, so we have to clean them
for (contact in contacts) {
contact.friend = null
}
contacts.clear()
contacts.addAll(all)
updateLocalContacts()
Log.i("[Contacts Manager] Async fetching finished, notifying observers")
notifyListeners()
}
@Synchronized
fun getAndroidContactIdFromUri(uri: Uri): String? {
val projection = arrayOf(ContactsContract.Data.CONTACT_ID)
@ -202,101 +143,28 @@ class ContactsManager(private val context: Context) {
}
@Synchronized
fun findContactById(id: String): Contact? {
var found: Contact? = null
if (contacts.isNotEmpty()) {
found = contacts.find { contact ->
contact is NativeContact && contact.nativeId == id
}
}
if (found == null) {
Log.i("[Contacts Manager] Contact with id $id not found yet")
} else {
Log.d("[Contacts Manager] Found contact with id [$id]: ${found.fullName}")
}
return found
fun findContactById(id: String): Friend? {
return coreContext.core.defaultFriendList?.findFriendByRefKey(id)
}
@Synchronized
fun findContactByPhoneNumber(number: String): Contact? {
val contact = friendsMap[number]
if (contact != null) return contact
val friend = coreContext.core.findFriendByPhoneNumber(number)
val udContact = friend?.userData as? Contact
if (udContact != null) friendsMap[number] = udContact
return udContact
fun findContactByPhoneNumber(number: String): Friend? {
return coreContext.core.findFriendByPhoneNumber(number)
}
@Synchronized
fun findContactByFriend(friend: Friend): Contact? {
val refKey = friend.refKey
if (refKey != null) {
val contact = findContactById(refKey)
if (contact != null) return contact
}
val address = friend.address
var potentialContact: Contact? = null
if (address != null) {
val friends = coreContext.core.findFriends(address)
for (f in friends) {
if (f.name == friend.name) {
val contact: Contact? = f.userData as? Contact
if (contact != null) return contact
} else {
val contact: Contact? = f.userData as? Contact
if (contact != null) potentialContact = contact
}
fun findContactByAddress(address: Address): Friend? {
for (friend in localFriends) {
val found = friend.addresses.find {
it.weakEqual(address)
}
if (found != null) {
return friend
}
}
if (potentialContact != null) {
return potentialContact
}
for (list in coreContext.core.friendsLists) {
for (f in list.friends) {
if (f.name == friend.name) {
val contact: Contact? = f.userData as? Contact
if (contact != null) return contact
}
}
}
return null
}
@Synchronized
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
val cleanStringAddress = cleanAddress.asStringUriOnly()
val cacheContact = friendsMap[cleanStringAddress]
if (cacheContact != null) {
return cacheContact
}
val friends = coreContext.core.findFriends(address)
for (friend in friends) {
val contact: Contact? = friend?.userData as? Contact
if (contact != null) {
friendsMap[cleanStringAddress] = contact
return contact
}
}
val friend = coreContext.core.findFriend(address)
if (friend != null) return friend
val username = address.username
if (username != null && Patterns.PHONE.matcher(username).matches()) {
@ -325,26 +193,15 @@ class ContactsManager(private val context: Context) {
}
@Synchronized
fun notifyListeners(contact: Contact) {
fun notifyListeners(friend: Friend) {
val list = contactsUpdatedListeners.toMutableList()
for (listener in list) {
listener.onContactUpdated(contact)
listener.onContactUpdated(friend)
}
}
@Synchronized
fun destroy() {
context.contentResolver.unregisterContentObserver(contactsObserver)
loadContactsTask?.cancel(true)
friendsMap.clear()
// Contact has a Friend field and Friend can have a Contact has userData
// Friend also keeps a ref on the Core, so we have to clean them
for (contact in contacts) {
contact.friend = null
}
contacts.clear()
val core = coreContext.core
for (list in core.friendsLists) list.removeListener(friendListListener)
}
@ -403,20 +260,13 @@ class ContactsManager(private val context: Context) {
@Synchronized
private fun refreshContactOnPresenceReceived(friend: Friend) {
if (friend.userData == null) return
val contact: Contact = friend.userData as Contact
Log.d("[Contacts Manager] Received presence information for contact $contact")
Log.d("[Contacts Manager] Received presence information for contact $friend")
if (corePreferences.storePresenceInNativeContact && PermissionHelper.get().hasWriteContactsPermission()) {
if (contact is NativeContact) {
storePresenceInNativeContact(contact)
if (friend.refKey != null) {
storePresenceInNativeContact(friend)
}
}
if (loadContactsTask?.status == AsyncTask.Status.RUNNING) {
Log.w("[Contacts Manager] Async contacts loader running, skip onContactUpdated listener notify")
} else {
notifyListeners(contact)
}
notifyListeners(friend)
}
@Synchronized
@ -424,33 +274,28 @@ class ContactsManager(private val context: Context) {
if (corePreferences.storePresenceInNativeContact && PermissionHelper.get().hasWriteContactsPermission()) {
for (list in coreContext.core.friendsLists) {
for (friend in list.friends) {
if (friend.userData == null) continue
val contact: Contact = friend.userData as Contact
if (contact is NativeContact) {
storePresenceInNativeContact(contact)
if (loadContactsTask?.status == AsyncTask.Status.RUNNING) {
Log.w("[Contacts Manager] Async contacts loader running, skip onContactUpdated listener notify")
} else {
notifyListeners(contact)
}
val id = friend.refKey
if (id != null) {
storePresenceInNativeContact(friend)
notifyListeners(friend)
}
}
}
}
}
private fun storePresenceInNativeContact(contact: NativeContact) {
for (phoneNumber in contact.rawPhoneNumbers) {
val sipAddress = contact.getContactForPhoneNumberOrAddress(phoneNumber)
private fun storePresenceInNativeContact(friend: Friend) {
for (phoneNumber in friend.phoneNumbersWithLabel) {
val sipAddress = friend.getContactForPhoneNumberOrAddress(phoneNumber.phoneNumber)
if (sipAddress != null) {
Log.d("[Contacts Manager] Found presence information to store in native contact $contact under Linphone sync account")
val contactEditor = NativeContactEditor(contact)
Log.d("[Contacts Manager] Found presence information to store in native contact $friend under Linphone sync account")
val contactEditor = NativeContactEditor(friend)
val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch {
val deferred = async {
withContext(Dispatchers.IO) {
contactEditor.setPresenceInformation(
phoneNumber,
phoneNumber.phoneNumber,
sipAddress
).commit()
}
@ -460,4 +305,98 @@ class ContactsManager(private val context: Context) {
}
}
}
fun createFriendFromSearchResult(searchResult: SearchResult): Friend {
val searchResultFriend = searchResult.friend
if (searchResultFriend != null) return searchResultFriend
val friend = coreContext.core.createFriend()
val address = searchResult.address
if (address != null) {
friend.address = address
}
val number = searchResult.phoneNumber
if (number != null) {
friend.addPhoneNumber(number)
if (address != null && address.username == number) {
friend.removeAddress(address)
}
}
return friend
}
}
fun Friend.getContactForPhoneNumberOrAddress(value: String): String? {
val presenceModel = getPresenceModelForUriOrTel(value)
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return presenceModel.contact
return null
}
fun Friend.hasPresence(): Boolean {
for (address in addresses) {
val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly())
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
}
for (number in phoneNumbersWithLabel) {
val presenceModel = getPresenceModelForUriOrTel(number.phoneNumber)
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
}
return false
}
fun Friend.getPictureUri(): Uri? {
val refKey = refKey
if (refKey != null) {
try {
val nativeId = refKey.toLong()
return Uri.withAppendedPath(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId),
ContactsContract.Contacts.Photo.DISPLAY_PHOTO
)
} catch (nfe: NumberFormatException) {}
}
val photoUri = photo ?: return null
return Uri.parse(photoUri)
}
fun Friend.getThumbnailUri(): Uri? {
val refKey = refKey
if (refKey != null) {
try {
val nativeId = refKey.toLong()
return Uri.withAppendedPath(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId),
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
)
} catch (nfe: NumberFormatException) {}
}
val photoUri = photo ?: return null
return Uri.parse(photoUri)
}
fun Friend.getPerson(): Person {
val personBuilder = Person.Builder().setName(name)
val bm: Bitmap? =
ImageUtils.getRoundBitmapFromUri(
coreContext.context,
getPictureUri()
)
val icon =
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.voip_single_contact_avatar_alt
) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
personBuilder.setImportant(starred)
return personBuilder.build()
}

View file

@ -133,7 +133,7 @@ open class ContactsSelectionViewModel : MessageNotifierViewModel() {
val contact = coreContext.contactsManager.findContactByAddress(address)
if (contact != null) {
val clone = address.clone()
clone.displayName = contact.fullName
clone.displayName = contact.name
list.add(clone)
} else {
list.add(address)

View file

@ -1,274 +0,0 @@
/*
* Copyright (c) 2010-2020 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.contact
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.provider.ContactsContract
import android.util.Patterns
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.SubscribePolicy
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.ImageUtils
import org.linphone.utils.LinphoneUtils
class NativeContact(val nativeId: String, private val lookupKey: String? = null) : Contact() {
override fun compareTo(other: Contact): Int {
val superResult = super.compareTo(other)
if (superResult == 0 && other is NativeContact) {
return nativeId.compareTo(other.nativeId)
}
return superResult
}
override fun getContactThumbnailPictureUri(): Uri {
return Uri.withAppendedPath(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId.toLong()),
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
)
}
override fun getContactPictureUri(): Uri {
return Uri.withAppendedPath(
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, nativeId.toLong()),
ContactsContract.Contacts.Photo.DISPLAY_PHOTO
)
}
override fun getPerson(): Person {
val personBuilder = Person.Builder().setName(fullName)
val bm: Bitmap? =
ImageUtils.getRoundBitmapFromUri(
coreContext.context,
getContactThumbnailPictureUri()
)
val icon =
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.voip_single_contact_avatar_alt
) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
personBuilder.setImportant(isStarred)
if (lookupKey != null) {
personBuilder.setUri("${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey")
}
return personBuilder.build()
}
@Synchronized
override fun syncValuesFromAndroidCursor(cursor: Cursor) {
try {
val displayName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME_PRIMARY))
val mime: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE))
val data1: String? = cursor.getString(cursor.getColumnIndexOrThrow("data1"))
val data2: String? = cursor.getString(cursor.getColumnIndexOrThrow("data2"))
val data3: String? = cursor.getString(cursor.getColumnIndexOrThrow("data3"))
val data4: String? = cursor.getString(cursor.getColumnIndexOrThrow("data4"))
if (fullName == null || fullName != displayName) {
Log.d("[Native Contact] Setting display name $displayName")
fullName = displayName
}
val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type)
when (mime) {
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
if (data1 == null && data4 == null) {
Log.d("[Native Contact] Phone number data is empty")
return
}
val labelColumnIndex =
cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)
val label: String? = cursor.getString(labelColumnIndex)
val typeColumnIndex =
cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE)
val type: Int = cursor.getInt(typeColumnIndex)
val typeLabel = ContactsContract.CommonDataKinds.Phone.getTypeLabel(
coreContext.context.resources,
type,
label
).toString()
// data4 = ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
// data1 = ContactsContract.CommonDataKinds.Phone.NUMBER
val number = if (corePreferences.preferNormalizedPhoneNumbersFromAddressBook ||
data1.isNullOrEmpty() ||
!Patterns.PHONE.matcher(data1).matches()
) {
data4 ?: data1
} else {
data1
}
if (number != null && number.isNotEmpty()) {
Log.d("[Native Contact] Found phone number $data1 ($data4), type label is $typeLabel")
val trimmedNumber = LinphoneUtils.trimPhoneNumber(number)
val found = rawPhoneNumbers.find { LinphoneUtils.trimPhoneNumber(it) == trimmedNumber }
if (found == null) {
phoneNumbers.add(PhoneNumber(number, typeLabel))
rawPhoneNumbers.add(number)
}
}
}
linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
if (data1 == null) {
Log.d("[Native Contact] SIP address is null")
return
}
Log.d("[Native Contact] Found SIP address $data1")
if (rawPhoneNumbers.contains(data1)) {
Log.d("[Native Contact] SIP address value already exists in phone numbers list, skipping")
return
}
val address: Address? = coreContext.core.interpretUrl(data1)
if (address == null) {
Log.e("[Native Contact] Couldn't parse address $data1 !")
return
}
val stringAddress = address.asStringUriOnly()
Log.d("[Native Contact] Found SIP address $stringAddress")
if (!rawSipAddresses.contains(data1)) {
sipAddresses.add(address)
rawSipAddresses.add(data1)
}
}
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
if (data1 == null) {
Log.d("[Native Contact] Organization is null")
return
}
Log.d("[Native Contact] Found organization $data1")
organization = data1
}
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
if (data2 == null && data3 == null) {
Log.d("[Native Contact] First name and last name are both null")
return
}
Log.d("[Native Contact] Found first name $data2 and last name $data3")
firstName = data2
lastName = data3
}
}
} catch (iae: IllegalArgumentException) {
Log.e("[Native Contact] Exception: $iae")
}
}
@Synchronized
fun createOrUpdateFriendFromNativeContact() {
var created = false
if (friend == null) {
val friend = coreContext.core.createFriend()
friend.isSubscribesEnabled = false
friend.incSubscribePolicy = SubscribePolicy.SPDeny
friend.refKey = nativeId
friend.userData = this
created = true
this.friend = friend
}
val friend = this.friend
if (friend != null) {
friend.edit()
val fn = fullName
if (fn != null) friend.name = fn
val vCard = friend.vcard
if (vCard != null) {
vCard.familyName = lastName
vCard.givenName = firstName
vCard.organization = organization
}
if (!created) {
for (address in friend.addresses) friend.removeAddress(address)
for (number in friend.phoneNumbers) friend.removePhoneNumber(number)
}
for (address in sipAddresses) friend.addAddress(address)
for (number in rawPhoneNumbers) friend.addPhoneNumber(number)
friend.done()
if (created) {
coreContext.core.defaultFriendList?.addFriend(friend)
}
}
}
@Synchronized
fun syncValuesFromAndroidContact(context: Context) {
Log.d("[Native Contact] Looking for contact cursor with id: $nativeId")
var selection: String = ContactsContract.Data.CONTACT_ID + " == " + nativeId
if (corePreferences.fetchContactsFromDefaultDirectory) {
Log.d("[Native Contact] Only fetching contacts in default directory")
selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1 AND " + selection
}
val cursor: Cursor? = context.contentResolver
.query(
ContactsContract.Data.CONTENT_URI,
AsyncContactsLoader.projection,
selection,
null,
null
)
if (cursor != null) {
sipAddresses.clear()
rawSipAddresses.clear()
phoneNumbers.clear()
rawPhoneNumbers.clear()
while (cursor.moveToNext()) {
syncValuesFromAndroidCursor(cursor)
}
cursor.close()
}
}
override fun toString(): String {
return "${super.toString()}: id [$nativeId], name [$fullName]"
}
}

View file

@ -28,11 +28,12 @@ import android.provider.ContactsContract.RawContacts
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.PermissionHelper
class NativeContactEditor(val contact: NativeContact) {
class NativeContactEditor(val friend: Friend) {
companion object {
fun createAndroidContact(accountName: String?, accountType: String?): Long {
Log.i("[Native Contact Editor] Using sync account $accountName with type $accountType")
@ -94,7 +95,7 @@ class NativeContactEditor(val contact: NativeContact) {
RawContacts.CONTENT_URI,
arrayOf(RawContacts._ID),
"${RawContacts.CONTACT_ID} =?",
arrayOf(contact.nativeId),
arrayOf(friend.refKey),
null
)
if (cursor?.moveToFirst() == true) {
@ -102,7 +103,7 @@ class NativeContactEditor(val contact: NativeContact) {
if (rawId == null) {
try {
rawId = cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID))
Log.i("[Native Contact Editor] Found raw id $rawId for native contact with id ${contact.nativeId}")
Log.i("[Native Contact Editor] Found raw id $rawId for native contact with id ${friend.refKey}")
} catch (iae: IllegalArgumentException) {
Log.e("[Native Contact Editor] Exception: $iae")
}
@ -113,12 +114,12 @@ class NativeContactEditor(val contact: NativeContact) {
}
fun setFirstAndLastNames(firstName: String, lastName: String): NativeContactEditor {
if (firstName == contact.firstName && lastName == contact.lastName) {
if (firstName == friend.vcard?.givenName && lastName == friend.vcard?.familyName) {
Log.w("[Native Contact Editor] First & last names haven't changed")
return this
}
val builder = if (contact.firstName == null && contact.lastName == null) {
val builder = if (friend.vcard?.givenName == null && friend.vcard?.familyName == null) {
// Probably a contact creation
ContentProviderOperation.newInsert(contactUri)
.withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId)
@ -126,7 +127,7 @@ class NativeContactEditor(val contact: NativeContact) {
ContentProviderOperation.newUpdate(contactUri)
.withSelection(
selection,
arrayOf(contact.nativeId, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
arrayOf(friend.refKey, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
)
}
@ -145,18 +146,18 @@ class NativeContactEditor(val contact: NativeContact) {
}
fun setOrganization(value: String): NativeContactEditor {
val previousValue = contact.organization
val previousValue = friend.vcard?.organization.orEmpty()
if (value == previousValue) {
Log.d("[Native Contact Editor] Organization hasn't changed")
return this
}
val builder = if (previousValue?.isNotEmpty() == true) {
val builder = if (previousValue.isNotEmpty()) {
ContentProviderOperation.newUpdate(contactUri)
.withSelection(
"$selection AND ${CommonDataKinds.Organization.COMPANY} =?",
arrayOf(
contact.nativeId,
friend.refKey,
CommonDataKinds.Organization.CONTENT_ITEM_TYPE,
previousValue
)
@ -261,7 +262,7 @@ class NativeContactEditor(val contact: NativeContact) {
RawContacts.CONTENT_URI,
arrayOf(RawContacts._ID, RawContacts.ACCOUNT_TYPE),
"${RawContacts.CONTACT_ID} =?",
arrayOf(contact.nativeId),
arrayOf(friend.refKey),
null
)
if (cursor?.moveToFirst() == true) {
@ -272,7 +273,7 @@ class NativeContactEditor(val contact: NativeContact) {
if (accountType == AppUtils.getString(R.string.sync_account_type) && syncAccountRawId == null) {
syncAccountRawId =
cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID))
Log.d("[Native Contact Editor] Found linphone raw id $syncAccountRawId for native contact with id ${contact.nativeId}")
Log.d("[Native Contact Editor] Found linphone raw id $syncAccountRawId for native contact with id ${friend.refKey}")
}
} catch (iae: IllegalArgumentException) {
Log.e("[Native Contact Editor] Exception: $iae")
@ -369,7 +370,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
phoneNumberSelection,
arrayOf(
contact.nativeId,
friend.refKey,
CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
currentValue,
currentValue
@ -390,7 +391,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
phoneNumberSelection,
arrayOf(
contact.nativeId,
friend.refKey,
CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
phoneNumber,
phoneNumber
@ -417,7 +418,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
"${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =? AND data1=?",
arrayOf(
contact.nativeId,
friend.refKey,
AppUtils.getString(R.string.linphone_address_mime_type),
currentValue
)
@ -431,7 +432,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
"${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =? AND data1=?",
arrayOf(
contact.nativeId,
friend.refKey,
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE,
currentValue
)
@ -448,7 +449,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
"${ContactsContract.Data.CONTACT_ID} =? AND (${ContactsContract.Data.MIMETYPE} =? OR ${ContactsContract.Data.MIMETYPE} =?) AND data1=?",
arrayOf(
contact.nativeId,
friend.refKey,
CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE,
AppUtils.getString(R.string.linphone_address_mime_type),
sipAddress
@ -518,7 +519,7 @@ class NativeContactEditor(val contact: NativeContact) {
.withSelection(
presenceUpdateSelection,
arrayOf(
contact.nativeId,
friend.refKey,
AppUtils.getString(R.string.linphone_address_mime_type),
phoneNumber
)

View file

@ -33,7 +33,8 @@ import android.util.Pair
import android.view.*
import androidx.emoji.bundled.BundledEmojiCompatConfig
import androidx.emoji.text.EmojiCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.*
import androidx.loader.app.LoaderManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import java.io.File
import java.math.BigInteger
@ -53,8 +54,9 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.compatibility.Compatibility
import org.linphone.compatibility.PhoneStateInterface
import org.linphone.contact.Contact
import org.linphone.contact.ContactLoader
import org.linphone.contact.ContactsManager
import org.linphone.contact.getContactForPhoneNumberOrAddress
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.notifications.NotificationsManager
@ -62,7 +64,21 @@ import org.linphone.telecom.TelecomHelper
import org.linphone.utils.*
import org.linphone.utils.Event
class CoreContext(val context: Context, coreConfig: Config) {
class CoreContext(val context: Context, coreConfig: Config) : LifecycleOwner, ViewModelStoreOwner {
private val _lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle {
return _lifecycleRegistry
}
private val _viewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore {
return _viewModelStore
}
private val contactLoader = ContactLoader()
private val collator: Collator = Collator.getInstance()
var stopped = false
val core: Core
val handler: Handler = Handler(Looper.getMainLooper())
@ -84,10 +100,10 @@ class CoreContext(val context: Context, coreConfig: Config) {
"$sdkVersion ($sdkBranch, $sdkBuildType)"
}
val collator: Collator = Collator.getInstance()
val contactsManager: ContactsManager by lazy {
ContactsManager(context)
}
val notificationsManager: NotificationsManager by lazy {
NotificationsManager(context)
}
@ -109,7 +125,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
Log.i("[Context] Global state changed [$state]")
if (state == GlobalState.On) {
contactsManager.fetchContactsAsync()
fetchContacts()
}
}
@ -150,8 +166,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
answerCall(call)
} else {
Log.i("[Context] Scheduling auto answering in $autoAnswerDelay milliseconds")
val mainThreadHandler = Handler(Looper.getMainLooper())
mainThreadHandler.postDelayed(
handler.postDelayed(
{
Log.w("[Context] Auto answering call")
answerCall(call)
@ -277,6 +292,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
core = Factory.instance().createCoreWithConfig(coreConfig, context)
stopped = false
_lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
Log.i("[Context] Ready")
}
@ -300,7 +316,9 @@ class CoreContext(val context: Context, coreConfig: Config) {
configureCore()
_lifecycleRegistry.currentState = Lifecycle.State.CREATED
core.start()
_lifecycleRegistry.currentState = Lifecycle.State.STARTED
initPhoneStateListener()
@ -318,6 +336,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
notificationsManager.startForeground()
}
_lifecycleRegistry.currentState = Lifecycle.State.RESUMED
Log.i("[Context] Started")
}
@ -339,6 +358,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
core.stop()
core.removeListener(listener)
stopped = true
_lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
// TODO: FIXME: uncomment
// loggingService.removeListener(loggingServiceListener)
}
@ -440,6 +460,13 @@ class CoreContext(val context: Context, coreConfig: Config) {
core.userCertificatesPath = userCertsPath
}
fun fetchContacts() {
if (PermissionHelper.required(context).hasReadContactsPermission()) {
Log.i("[Context] Init contacts loader")
LoaderManager.getInstance(this@CoreContext).initLoader(0, null, contactLoader)
}
}
/* Call related functions */
fun initPhoneStateListener() {
@ -552,7 +579,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
fun startCall(to: String) {
var stringAddress = to
if (android.util.Patterns.PHONE.matcher(to).matches()) {
val contact: Contact? = contactsManager.findContactByPhoneNumber(to)
val contact = contactsManager.findContactByPhoneNumber(to)
val alias = contact?.getContactForPhoneNumberOrAddress(to)
if (alias != null) {
Log.i("[Context] Found matching alias $alias for phone number $to, using it")

View file

@ -45,7 +45,8 @@ import org.linphone.activities.chat_bubble.ChatBubbleActivity
import org.linphone.activities.main.MainActivity
import org.linphone.activities.voip.CallActivity
import org.linphone.compatibility.Compatibility
import org.linphone.contact.Contact
import org.linphone.contact.getPerson
import org.linphone.contact.getThumbnailUri
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
@ -65,7 +66,7 @@ class Notifiable(val notificationId: Int) {
class NotifiableMessage(
var message: String,
val contact: Contact?,
val friend: Friend?,
val sender: String,
val time: Long,
val senderAvatar: Bitmap? = null,
@ -421,9 +422,9 @@ class NotificationsManager(private val context: Context) {
return notifiable
}
fun getPerson(contact: Contact?, displayName: String, picture: Bitmap?): Person {
return if (contact != null) {
contact.getPerson()
fun getPerson(friend: Friend?, displayName: String, picture: Bitmap?): Person {
return if (friend != null) {
friend.getPerson()
} else {
val builder = Person.Builder().setName(displayName)
val userIcon =
@ -488,9 +489,9 @@ class NotificationsManager(private val context: Context) {
.format(missedCallCount)
Log.i("[Notifications Manager] Updating missed calls notification count to $missedCallCount")
} else {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(remoteAddress)
val friend: Friend? = coreContext.contactsManager.findContactByAddress(remoteAddress)
body = context.getString(R.string.missed_call_notification_body)
.format(contact?.fullName ?: LinphoneUtils.getDisplayName(remoteAddress))
.format(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress))
Log.i("[Notifications Manager] Creating missed call notification")
}
@ -613,15 +614,15 @@ class NotificationsManager(private val context: Context) {
}
private fun displayIncomingChatNotification(room: ChatRoom, message: ChatMessage) {
val contact: Contact? = coreContext.contactsManager.findContactByAddress(message.fromAddress)
val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress)
val notifiable = getNotifiableForRoom(room)
if (notifiable.messages.isNotEmpty() || room.unreadMessagesCount == 1) {
val notifiableMessage = getNotifiableMessage(message, contact)
val notifiableMessage = getNotifiableMessage(message, friend)
notifiable.messages.add(notifiableMessage)
} else {
for (chatMessage in room.unreadHistory) {
val notifiableMessage = getNotifiableMessage(chatMessage, contact)
val notifiableMessage = getNotifiableMessage(chatMessage, friend)
notifiable.messages.add(notifiableMessage)
}
}
@ -650,10 +651,9 @@ class NotificationsManager(private val context: Context) {
return notifiable
}
private fun getNotifiableMessage(message: ChatMessage, contact: Contact?): NotifiableMessage {
val pictureUri = contact?.getContactThumbnailPictureUri()
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(message.fromAddress)
private fun getNotifiableMessage(message: ChatMessage, friend: Friend?): NotifiableMessage {
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, friend?.getThumbnailUri())
val displayName = friend?.name ?: LinphoneUtils.getDisplayName(message.fromAddress)
var text = ""
val isConferenceInvite = message.contents.firstOrNull()?.isIcalendar ?: false
@ -671,7 +671,7 @@ class NotificationsManager(private val context: Context) {
val notifiableMessage = NotifiableMessage(
text,
contact,
friend,
displayName,
message.time,
senderAvatar = roundPicture,
@ -765,8 +765,8 @@ class NotificationsManager(private val context: Context) {
var lastPerson: Person? = null
for (message in notifiable.messages) {
val contact = message.contact
val person = getPerson(contact, message.sender, message.senderAvatar)
val friend = message.friend
val person = getPerson(friend, message.sender, message.senderAvatar)
// We don't want to see our own avatar
if (!message.isOutgoing) {

View file

@ -33,7 +33,6 @@ import android.telecom.TelecomManager.*
import java.lang.Exception
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contact.Contact
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
@ -228,8 +227,8 @@ class TelecomHelper private constructor(context: Context) {
extras.putString("Call-ID", call.callLog.callId)
val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
extras.putString("DisplayName", displayName)
return extras

View file

@ -42,13 +42,13 @@ class ContactUtils {
return null
}
val vcard = contact.friend?.vcard?.asVcard4String()
val vcard = contact.vcard?.asVcard4String()
if (vcard == null) {
Log.e("[Contact Utils] Failed to get vCard from contact $contactID")
return null
}
val contactName = contact.fullName?.replace(" ", "_") ?: contactID
val contactName = contact.name?.replace(" ", "_") ?: contactID
val vcardPath = FileUtils.getFileStoragePath("$contactName.vcf")
val inputStream = ByteArrayInputStream(vcard.toByteArray())
try {

View file

@ -176,7 +176,11 @@ class LinphoneUtils {
return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}"
}
fun trimPhoneNumber(phoneNumber: String): String {
fun arePhoneNumberWeakEqual(number1: String, number2: String): Boolean {
return trimPhoneNumber(number1) == trimPhoneNumber(number2)
}
private fun trimPhoneNumber(phoneNumber: String): String {
return phoneNumber.replace(" ", "")
.replace("-", "")
.replace("(", "")

View file

@ -33,11 +33,11 @@ import androidx.core.graphics.drawable.IconCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.main.MainActivity
import org.linphone.contact.Contact
import org.linphone.contact.NativeContact
import org.linphone.contact.getPerson
import org.linphone.core.Address
import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoomCapabilities
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
@ -78,10 +78,10 @@ class ShortcutsHelper(val context: Context) {
val stringAddress = address.asStringUriOnly()
if (!processedAddresses.contains(stringAddress)) {
processedAddresses.add(stringAddress)
val contact: Contact? =
val contact: Friend? =
coreContext.contactsManager.findContactByAddress(address)
if (contact != null && contact is NativeContact) {
if (contact != null && contact.refKey != null) {
val shortcut: ShortcutInfo? = createContactShortcut(context, contact)
if (shortcut != null) {
Log.i("[Shortcut Helper] Creating launcher shortcut for ${shortcut.shortLabel}")
@ -97,27 +97,28 @@ class ShortcutsHelper(val context: Context) {
shortcutManager.dynamicShortcuts = shortcuts
}
private fun createContactShortcut(context: Context, contact: NativeContact): ShortcutInfo? {
private fun createContactShortcut(context: Context, contact: Friend): ShortcutInfo? {
try {
val categories: ArraySet<String> = ArraySet()
categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION)
val person = contact.getPerson()
val icon = person.icon
val id = contact.refKey ?: return null
val intent = Intent(Intent.ACTION_MAIN)
intent.setClass(context, MainActivity::class.java)
intent.putExtra("ContactId", contact.nativeId)
intent.putExtra("ContactId", id)
return ShortcutInfoCompat.Builder(context, contact.nativeId)
.setShortLabel(contact.fullName ?: "${contact.firstName} ${contact.lastName}")
return ShortcutInfoCompat.Builder(context, id)
.setShortLabel(contact.name ?: "")
.setIcon(icon)
.setPerson(person)
.setCategories(categories)
.setIntent(intent)
.build().toShortcutInfo()
} catch (e: Exception) {
Log.e("[Shortcuts Helper] createContactShortcut for contact [${contact.fullName}] exception: $e")
Log.e("[Shortcuts Helper] createContactShortcut for contact [${contact.name}] exception: $e")
}
return null
@ -168,7 +169,7 @@ class ShortcutsHelper(val context: Context) {
if (contact != null) {
personsList.add(contact.getPerson())
}
subject = contact?.fullName ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)
subject = contact?.name ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)
icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar_alt)
} else if (chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) && chatRoom.participants.isNotEmpty()) {
val address = chatRoom.participants.first().address
@ -177,7 +178,7 @@ class ShortcutsHelper(val context: Context) {
if (contact != null) {
personsList.add(contact.getPerson())
}
subject = contact?.fullName ?: LinphoneUtils.getDisplayName(address)
subject = contact?.name ?: LinphoneUtils.getDisplayName(address)
icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar_alt)
} else {
for (participant in chatRoom.participants) {

View file

@ -55,7 +55,7 @@
android:paddingLeft="5dp">
<org.linphone.views.MarqueeTextView
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.fullName ?? viewModel.displayName) : viewModel.subject}"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.name ?? viewModel.displayName) : viewModel.subject}"
style="@style/toolbar_small_title_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -235,7 +235,7 @@
<TextView
android:id="@+id/time"
android:text="@{data.chatMessage.outgoing ? data.time : data.time + ` - ` + (data.contact.fullName ?? data.displayName)}"
android:text="@{data.chatMessage.outgoing ? data.time : data.time + ` - ` + (data.contact.name ?? data.displayName)}"
android:visibility="@{data.hideTime ? View.GONE : View.VISIBLE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -35,7 +35,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/chat_message_reply_sender_font"
android:text="@{data.contact.fullName ?? data.displayName, default=Tintin}"/>
android:text="@{data.contact.name ?? data.displayName, default=Tintin}"/>
<HorizontalScrollView
android:visibility="@{data.contents.size() == 0 ? View.GONE : View.VISIBLE}"

View file

@ -61,7 +61,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/chat_message_reply_sender_font"
android:text="@{data.contact.fullName ?? data.displayName, default=Tintin}"/>
android:text="@{data.contact.name ?? data.displayName, default=Tintin}"/>
<TextView
android:visibility="@{data.text.length() == 0 ? View.GONE : View.VISIBLE}"

View file

@ -78,7 +78,7 @@
<org.linphone.views.MarqueeTextView
android:onClick="@{titleClickListener}"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.fullName ?? viewModel.displayName) : viewModel.subject}"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.name ?? viewModel.displayName) : viewModel.subject}"
style="@style/toolbar_small_title_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -47,7 +47,7 @@
android:orientation="vertical">
<org.linphone.views.MarqueeTextView
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -121,7 +121,7 @@
android:orientation="vertical">
<org.linphone.views.MarqueeTextView
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -44,7 +44,7 @@
android:orientation="vertical">
<org.linphone.views.MarqueeTextView
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -74,7 +74,7 @@
<TextView
android:id="@+id/title"
style="@style/contact_name_list_cell_font"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.fullName ?? viewModel.displayName) : viewModel.subject}"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.name ?? viewModel.displayName) : viewModel.subject}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="30dp"

View file

@ -46,7 +46,7 @@
android:layout_height="wrap_content"
android:gravity="top|left"
android:lines="1"
android:text="@{data.contact.fullName ?? data.displayName}" />
android:text="@{data.contact.name ?? data.displayName}" />
<TextView
style="@style/conference_scheduling_participant_sip_uri_font"

View file

@ -10,6 +10,9 @@
<variable
name="initials"
type="String" />
<variable
name="thumbnailPath"
type="android.net.Uri" />
<variable
name="imagePath"
type="android.net.Uri" />
@ -52,6 +55,15 @@
android:singleLine="true"
android:ellipsize="none" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:background="?attr/backgroundColor"
app:glideAvatar="@{thumbnailPath}" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -95,7 +95,7 @@
app:viewModel="@{viewModel}"/>
<TextView
android:text="@{viewModel.contact.fullName}"
android:text="@{viewModel.contact.name ?? viewModel.displayName}"
style="@style/big_contact_name_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -58,7 +58,7 @@
android:src="@drawable/linphone_logo_tinted"
android:layout_marginRight="10dp"
android:contentDescription="@string/content_description_linphone_user"
android:visibility="@{viewModel.contact.hasPresence() ? View.VISIBLE : View.GONE}" />
android:visibility="@{viewModel.hasPresence() ? View.VISIBLE : View.GONE}" />
<CheckBox
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"
@ -80,7 +80,7 @@
<org.linphone.views.MarqueeTextView
android:id="@+id/name"
android:text="@{viewModel.contact.fullName}"
android:text="@{viewModel.contact.name ?? viewModel.displayName}"
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -47,7 +47,7 @@
android:orientation="vertical">
<TextView
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
style="@style/contact_name_list_cell_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -63,7 +63,7 @@
<ImageView
android:onClick="@{contactClickListener}"
android:visibility="@{viewModel.contact == null ? View.GONE : View.VISIBLE}"
android:visibility="@{viewModel.contact == null || viewModel.contact.refKey == null ? View.GONE : View.VISIBLE}"
android:contentDescription="@string/content_description_go_to_contact"
android:layout_width="0dp"
android:layout_height="match_parent"
@ -97,7 +97,7 @@
app:viewModel="@{viewModel}"/>
<TextView
android:text="@{viewModel.contact.fullName ?? viewModel.displayName}"
android:text="@{viewModel.contact.name ?? viewModel.displayName}"
style="@style/big_contact_name_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -113,7 +113,7 @@
android:layout_toRightOf="@id/icon"
android:gravity="center_vertical"
android:singleLine="true"
android:text="@{(viewModel.isConferenceCallLog ? viewModel.conferenceSubject : viewModel.contact.fullName ?? viewModel.displayName) + (groupCount > 1 ? ` (` + groupCount + `)` : ``), default=`Frodo Baggins (6)`}" />
android:text="@{(viewModel.isConferenceCallLog ? viewModel.conferenceSubject : viewModel.contact.name ?? viewModel.displayName) + (groupCount > 1 ? ` (` + groupCount + `)` : ``), default=`Frodo Baggins (6)`}" />
</RelativeLayout>

View file

@ -79,7 +79,7 @@
android:layout_marginTop="15dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{callsViewModel.currentCallData.contact.fullName ?? callsViewModel.currentCallData.displayName, default=`Bilbo Baggins`}"
android:text="@{callsViewModel.currentCallData.contact.name ?? callsViewModel.currentCallData.displayName, default=`Bilbo Baggins`}"
app:layout_constraintBottom_toTopOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -75,7 +75,7 @@
android:layout_marginTop="15dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{callsViewModel.currentCallData.remoteConferenceSubject ?? callsViewModel.currentCallData.contact.fullName ?? callsViewModel.currentCallData.displayName, default=`Bilbo Baggins`}"
android:text="@{callsViewModel.currentCallData.remoteConferenceSubject ?? callsViewModel.currentCallData.contact.name ?? callsViewModel.currentCallData.displayName, default=`Bilbo Baggins`}"
app:layout_constraintBottom_toTopOf="@id/callee_address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -68,7 +68,7 @@
android:maxLines="1"
android:ellipsize="end"
style="@style/call_list_active_name_font"
android:text="@{data.contact.fullName ?? data.displayName, default=`Bilbo Baggins`}"/>
android:text="@{data.contact.name ?? data.displayName, default=`Bilbo Baggins`}"/>
<TextView
android:visibility="@{data.isInRemoteConference ? View.VISIBLE : View.GONE, default=gone}"
@ -104,7 +104,7 @@
android:maxLines="1"
android:ellipsize="end"
style="@style/call_list_name_font"
android:text="@{data.contact.fullName ?? data.displayName, default=`Bilbo Baggins`}"/>
android:text="@{data.contact.name ?? data.displayName, default=`Bilbo Baggins`}"/>
<TextView
android:visibility="@{data.isInRemoteConference ? View.VISIBLE : View.GONE, default=gone}"

View file

@ -41,7 +41,7 @@
android:orientation="vertical">
<org.linphone.views.MarqueeTextView
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.fullName ?? viewModel.displayName) : viewModel.subject}"
android:text="@{viewModel.oneToOneChatRoom ? (viewModel.contact.name ?? viewModel.displayName) : viewModel.subject}"
style="@style/toolbar_small_title_font"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -76,7 +76,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:text="@{conferenceViewModel.speakingParticipant.contact.fullName ?? conferenceViewModel.speakingParticipant.displayName}"
android:text="@{conferenceViewModel.speakingParticipant.contact.name ?? conferenceViewModel.speakingParticipant.displayName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -117,7 +117,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@{data.contact.fullName ?? data.displayName, default=`Bilbo Baggins`}" />
android:text="@{data.contact.name ?? data.displayName, default=`Bilbo Baggins`}" />
<org.linphone.views.MarqueeTextView
android:id="@+id/sipUri"

View file

@ -86,7 +86,7 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -43,7 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@{data.contact.fullName ?? data.displayName, default=`Merry Brandybuck`}"
android:text="@{data.contact.name ?? data.displayName, default=`Merry Brandybuck`}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/participant_avatar"
app:layout_constraintTop_toTopOf="parent" />

View file

@ -85,7 +85,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginBottom="15dp"
android:text="@{data.contact.fullName ?? data.displayName}"
android:text="@{data.contact.name ?? data.displayName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -50,7 +50,16 @@
android:adjustViewBounds="true"
android:contentDescription="@null"
android:background="?attr/voipParticipantBackgroundColor"
app:glideAvatar="@{data.contact.contactPictureUri}" />
app:glideAvatar="@{data.thumbnailUri}" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:background="?attr/voipParticipantBackgroundColor"
app:glideAvatar="@{data.pictureUri}" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -54,7 +54,16 @@
android:adjustViewBounds="true"
android:contentDescription="@null"
android:background="?attr/voipBackgroundColor"
app:glideAvatar="@{data.contact.contactPictureUri}" />
app:glideAvatar="@{data.thumbnailUri}" />
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:background="?attr/voipBackgroundColor"
app:glideAvatar="@{data.pictureUri}" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -55,7 +55,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="@{(callsViewModel.currentCallData.contact.fullName ?? callsViewModel.currentCallData.displayName) + ` - `, default=`John Doe - `}"
android:text="@{(callsViewModel.currentCallData.contact.name ?? callsViewModel.currentCallData.displayName) + ` - `, default=`John Doe - `}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -126,7 +126,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:text="@{callsViewModel.currentCallData.contact.fullName ?? callsViewModel.currentCallData.displayName}"
android:text="@{callsViewModel.currentCallData.contact.name ?? callsViewModel.currentCallData.displayName}"
app:layout_constraintBottom_toBottomOf="@id/background"
app:layout_constraintStart_toStartOf="@id/background" />