From 1ceb3de3da3ee624d70413088f5b1eb681280403 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 5 Apr 2022 16:26:56 +0200 Subject: [PATCH] Reworked native address book integration, removed Contact & NativeContact objects to directly rely on Friend --- CHANGELOG.md | 16 + .../org/linphone/activities/Navigation.kt | 9 +- .../chat/data/ChatRoomCreationContactData.kt | 3 +- .../activities/main/chat/data/EventData.kt | 2 +- .../fragments/ChatRoomCreationFragment.kt | 25 +- .../viewmodels/ChatRoomCreationViewModel.kt | 2 +- .../main/chat/viewmodels/ChatRoomViewModel.kt | 14 +- .../contact/adapters/ContactsListAdapter.kt | 18 +- .../fragments/ContactEditorFragment.kt | 7 +- .../fragments/DetailContactFragment.kt | 20 +- .../fragments/MasterContactsFragment.kt | 37 +- .../viewmodels/ContactEditorViewModel.kt | 105 ++--- .../contact/viewmodels/ContactViewModel.kt | 104 +++-- .../viewmodels/ContactsListViewModel.kt | 156 +++---- .../fragments/DetailCallLogFragment.kt | 9 +- .../history/viewmodels/CallLogViewModel.kt | 2 +- .../fragments/ContactsSettingsFragment.kt | 3 +- .../main/viewmodels/SharedMainViewModel.kt | 3 +- .../compatibility/Api26Compatibility.kt | 19 +- .../compatibility/Api31Compatibility.kt | 17 +- .../compatibility/XiaomiCompatibility.kt | 9 +- .../linphone/contact/AsyncContactsLoader.kt | 278 ------------- .../linphone/contact/BigContactAvatarView.kt | 11 +- .../main/java/org/linphone/contact/Contact.kt | 216 ---------- .../org/linphone/contact/ContactAvatarView.kt | 6 +- .../linphone/contact/ContactDataInterface.kt | 25 +- .../org/linphone/contact/ContactLoader.kt | 210 ++++++++++ .../org/linphone/contact/ContactsManager.kt | 393 +++++++----------- .../org/linphone/contact/NativeContact.kt | 269 ------------ .../linphone/contact/NativeContactEditor.kt | 35 +- .../java/org/linphone/core/CoreContext.kt | 44 +- .../notifications/NotificationsManager.kt | 34 +- .../org/linphone/telecom/TelecomHelper.kt | 5 +- .../java/org/linphone/utils/ContactUtils.kt | 4 +- .../java/org/linphone/utils/LinphoneUtils.kt | 14 +- .../org/linphone/utils/ShortcutsHelper.kt | 23 +- .../layout-land/call_controls_fragment.xml | 2 +- .../layout/call_conference_participant.xml | 2 +- .../res/layout/call_controls_fragment.xml | 2 +- .../res/layout/call_incoming_activity.xml | 4 +- .../res/layout/call_outgoing_activity.xml | 2 +- app/src/main/res/layout/call_paused.xml | 2 +- .../layout/call_statistics_cell_header.xml | 2 +- .../main/res/layout/chat_bubble_activity.xml | 2 +- .../res/layout/chat_message_list_cell.xml | 2 +- .../main/res/layout/chat_message_reply.xml | 2 +- .../res/layout/chat_message_reply_bubble.xml | 2 +- .../chat_room_creation_contact_cell.xml | 2 +- .../res/layout/chat_room_detail_fragment.xml | 2 +- .../layout/chat_room_devices_group_cell.xml | 2 +- .../chat_room_group_info_participant_cell.xml | 2 +- .../chat_room_imdn_participant_cell.xml | 2 +- .../main/res/layout/chat_room_list_cell.xml | 2 +- .../main/res/layout/contact_avatar_big.xml | 12 + .../res/layout/contact_detail_fragment.xml | 2 +- app/src/main/res/layout/contact_list_cell.xml | 4 +- .../res/layout/history_detail_fragment.xml | 4 +- app/src/main/res/layout/history_list_cell.xml | 2 +- 58 files changed, 837 insertions(+), 1370 deletions(-) delete mode 100644 app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt delete mode 100644 app/src/main/java/org/linphone/contact/Contact.kt create mode 100644 app/src/main/java/org/linphone/contact/ContactLoader.kt delete mode 100644 app/src/main/java/org/linphone/contact/NativeContact.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e2be463a..b854663e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,22 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [4.6.4] - Unreleased + +### Added +- Set video information in CallStyle incoming call notification + +### Changed +- Massive rework of how native contacts from address book are handled to improve performances +- Only display phone number from LDAP search result if it matches SIP address' username + +### Fixed +- Do not use CallStyle notification on Samsung devices, they are currently displayed badly +- Fixed microphone muted when starting a new call if microphone was muted at the end of the previous one +- Added LDAP contact display name to SIP address +- Prevent read-only 1-1 chat room +- Fixed chat room last updated time not updated sometimes + ## [4.6.3] - 2022-03-08 ### Added diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt index e5b69c538..788a27cec 100644 --- a/app/src/main/java/org/linphone/activities/Navigation.kt +++ b/app/src/main/java/org/linphone/activities/Navigation.kt @@ -45,7 +45,6 @@ import org.linphone.activities.main.history.fragments.DetailCallLogFragment import org.linphone.activities.main.history.fragments.MasterCallLogsFragment import org.linphone.activities.main.settings.fragments.* import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment -import org.linphone.contact.NativeContact import org.linphone.core.Address internal fun Fragment.findMasterNavController(): NavController { @@ -432,9 +431,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, @@ -524,8 +523,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)) } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt index 1c02c2e87..a6dbb7bf3 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt @@ -21,13 +21,12 @@ package org.linphone.activities.main.chat.data import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.contact.Contact import org.linphone.contact.ContactDataInterface import org.linphone.core.* import org.linphone.utils.LinphoneUtils class ChatRoomCreationContactData(private val searchResult: SearchResult) : ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() + override val contact: MutableLiveData = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt index eb7d0c9cf..4a7a0d792 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt @@ -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() { diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt index 95b08e3bf..c9117dafa 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt @@ -25,7 +25,7 @@ import android.view.View import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.activities.main.MainActivity import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter @@ -166,16 +166,6 @@ class ChatRoomCreationFragment : SecureFragment } } - 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, @@ -185,14 +175,23 @@ class ChatRoomCreationFragment : SecureFragment val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (granted) { Log.i("[Chat Room Creation] READ_CONTACTS permission granted") - LinphoneApplication.coreContext.contactsManager.onReadContactsPermissionGranted() - LinphoneApplication.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) { diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt index bb1cc356e..28be5e3b0 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt @@ -137,7 +137,7 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() { list.remove(found) } else { val contact = coreContext.contactsManager.findContactByAddress(address) - if (contact != null) address.displayName = contact.fullName + if (contact != null) address.displayName = contact.name list.add(address) } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt index f9c1523e2..7cd916994 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt @@ -27,7 +27,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.* @@ -46,7 +45,7 @@ class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) : } class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() + override val contact: MutableLiveData = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() override val showGroupChatAvatar: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && @@ -286,7 +285,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf if (msg == null) return "" val sender: String = - coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName + coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.name ?: LinphoneUtils.getDisplayName(msg.fromAddress) var body = "" for (content in msg.contents) { @@ -317,9 +316,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 += ", " } @@ -346,9 +344,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) } diff --git a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt index 56b2df12d..247faa07b 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt @@ -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(selectionVM, ContactDiffCallback()), HeaderAdapter { - val selectedContactEvent: MutableLiveData> by lazy { - MutableLiveData>() + val selectedContactEvent: MutableLiveData> by lazy { + MutableLiveData>() } 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() { oldItem: ContactViewModel, newItem: ContactViewModel ): Boolean { - return oldItem.contactInternal.compareTo(newItem.contactInternal) == 0 + return oldItem.fullName.compareTo(newItem.fullName) == 0 } override fun areContentsTheSame( diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt index ac738abd4..5e12c3637 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt @@ -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(), 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() } diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt index cf7384fae..373494334 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt @@ -70,8 +70,7 @@ class DetailContactFragment : GenericFragment() { val contact = sharedViewModel.selectedContact.value if (contact == null) { - Log.e("[Contact] Contact is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) + Log.e("[Contact] Friend is null, aborting!") goBack() return } @@ -147,6 +146,7 @@ class DetailContactFragment : GenericFragment() { (activity as MainActivity).showSnackBar(messageResourceId) } } + viewModel.updateNumbersAndAddresses() view.doOnPreDraw { // Notifies fragment is ready to be drawn @@ -154,6 +154,22 @@ class DetailContactFragment : GenericFragment() { } } + 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) { diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt index 20f03be09..382b76b82 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt @@ -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.* @@ -189,13 +189,15 @@ class MasterContactsFragment : MasterFragment - 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() @@ -233,6 +235,12 @@ class MasterContactsFragment : MasterFragment) { - val list = ArrayList() + val list = ArrayList() 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 @@ -349,8 +359,7 @@ class MasterContactsFragment : MasterFragment create(modelClass: Class): T { - return ContactEditorViewModel(contact) as T + return ContactEditorViewModel(friend) as T } } -class ContactEditorViewModel(val c: Contact?) : ViewModel(), ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() +class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface { + override val contact: MutableLiveData = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() @@ -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() - 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() - 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)) diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt index b2d266714..925724f07 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt @@ -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.ErrorReportingViewModel -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 create(modelClass: Class): T { - return ContactViewModel(contact) as T + return ContactViewModel(friend) as T } } -class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel(), ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() +class ContactViewModel(friend: Friend, async: Boolean = false) : ErrorReportingViewModel(), ContactDataInterface { + override val contact: MutableLiveData = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() - val name: String - get() = displayName.value ?: "" + var fullName = "" val displayOrganization = corePreferences.displayOrganization @@ -76,28 +74,33 @@ class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel() val isNativeContact = MutableLiveData() - 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 onErrorEvent.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) : ErrorReportingViewModel() } 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) : ErrorReportingViewModel() } 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() - 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) : ErrorReportingViewModel() ) } - 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) : ErrorReportingViewModel() } } - fun updateNumbersAndAddresses(contact: Contact) { + fun updateNumbersAndAddresses() { val list = arrayListOf() - 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 } } diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt index 8285e3b65..a138f79bb 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt @@ -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() @@ -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) { - Log.i("[Contacts] Processing ${results.size} results") + Log.i("[Contacts Loader] Processing ${results.size} results") contactsList.value.orEmpty().forEach(ContactViewModel::destroy) - val list = arrayListOf() - 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() + val viewModels = HashMap() - 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() - 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) { + fun deleteContacts(list: ArrayList) { val select = ContactsContract.Data.CONTACT_ID + " = ?" val ops = ArrayList() - 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 - } } diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt index 825921125..1a11cf0b9 100644 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt @@ -33,7 +33,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 @@ -86,10 +85,10 @@ class DetailCallLogFragment : GenericFragment() { 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() diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt index a5cabce8b..2409e102a 100644 --- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt @@ -59,7 +59,7 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r 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>() diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt index b5d0bc557..389615299 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt @@ -109,8 +109,7 @@ class ContactsSettingsFragment : GenericSettingFragment>() } - val selectedContact = MutableLiveData() + val selectedContact = MutableLiveData() // For correct animations directions val updateContactsAnimationsBasedOnDestination: MutableLiveData> by lazy { diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt index a6e7be818..1bcff76ff 100644 --- a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt @@ -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) diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt index 59632e3d0..99e53987b 100644 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt @@ -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.core.tools.Log import org.linphone.notifications.Notifiable @@ -50,10 +50,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() @@ -98,10 +97,10 @@ class Api31Compatibility { 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 = + coreContext.contactsManager.findContactByAddress(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) { diff --git a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt index 05b6a482c..a152a710c 100644 --- a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt @@ -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)) diff --git a/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt b/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt deleted file mode 100644 index c531b2d6e..000000000 --- a/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt +++ /dev/null @@ -1,278 +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 . - */ -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() { - 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 = HashMap() - val nativeIds = arrayListOf() - 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) - if (contact.sipAddresses.isNotEmpty()) { - data.sipContacts.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 (contact.sipAddresses.isNotEmpty()) { - data.sipContacts.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 = 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..." - ) - } - } - - if (!corePreferences.hideContactsWithoutPresence) { - if (contact.sipAddresses.isNotEmpty() && !data.sipContacts.contains(contact)) { - data.sipContacts.add(contact) - } - } - 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 found in which ${data.sipContacts.size} are SIP") - - for (contact in data.contacts) { - if (contact is NativeContact) { - contact.createOrUpdateFriendFromNativeContact() - - if (contact.friend?.presenceModel?.basicStatus == PresenceBasicStatus.Open && !data.sipContacts.contains(contact)) { - Log.i("[Contacts Loader] Friend $contact has presence information, adding it to SIP list") - data.sipContacts.add(contact) - } - } - } - data.sipContacts.sort() - - // 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, data.sipContacts) - - Log.i("[Contacts Loader] Synchronization finished") - } - - class AsyncContactsData { - val contacts = arrayListOf() - val sipContacts = arrayListOf() - } -} diff --git a/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt index a608d0c18..8b0041585 100644 --- a/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt +++ b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt @@ -25,7 +25,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.databinding.DataBindingUtil -import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.databinding.ContactAvatarBigBinding import org.linphone.utils.AppUtils @@ -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.borderVisibility = LinphoneApplication.corePreferences.showBorderOnBigContactAvatar + binding.imagePath = contact?.getPictureUri() + binding.thumbnailPath = contact?.getThumbnailUri() + binding.borderVisibility = corePreferences.showBorderOnBigContactAvatar } } diff --git a/app/src/main/java/org/linphone/contact/Contact.kt b/app/src/main/java/org/linphone/contact/Contact.kt deleted file mode 100644 index 23162ab9c..000000000 --- a/app/src/main/java/org/linphone/contact/Contact.kt +++ /dev/null @@ -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 . - */ -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 { - override fun compareTo(other: PhoneNumber): Int { - return value.compareTo(other.value) - } -} - -open class Contact() : Comparable { - var fullName: String? = null - var firstName: String? = null - var lastName: String? = null - var organization: String? = null - var isStarred: Boolean = false - - var phoneNumbers = arrayListOf() - var rawPhoneNumbers = arrayListOf() - var sipAddresses = arrayListOf
() - // Raw SIP addresses are only used for contact edition - var rawSipAddresses = arrayListOf() - - 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 null - } - - 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.avatar - ) 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]" - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactAvatarView.kt b/app/src/main/java/org/linphone/contact/ContactAvatarView.kt index 1078a99ff..1a7ee84b7 100644 --- a/app/src/main/java/org/linphone/contact/ContactAvatarView.kt +++ b/app/src/main/java/org/linphone/contact/ContactAvatarView.kt @@ -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) { diff --git a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt index 5e58786ed..f1047e87a 100644 --- a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt +++ b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt @@ -24,10 +24,12 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.activities.main.viewmodels.ErrorReportingViewModel 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 + val contact: MutableLiveData val displayName: MutableLiveData @@ -38,12 +40,14 @@ interface ContactDataInterface { } open class GenericContactData(private val sipAddress: Address) : ContactDataInterface { - final override val contact: MutableLiveData = MutableLiveData() + final override val contact: MutableLiveData = MutableLiveData() final override val displayName: MutableLiveData = MutableLiveData() final override val securityLevel: MutableLiveData = MutableLiveData() + val initials = MutableLiveData() + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactUpdated(contact: Contact) { + override fun onContactUpdated(friend: Friend) { contactLookup() } } @@ -60,18 +64,25 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte private fun contactLookup() { displayName.value = LinphoneUtils.getDisplayName(sipAddress) - contact.value = - coreContext.contactsManager.findContactByAddress(sipAddress) + + val c = coreContext.contactsManager.findContactByAddress(sipAddress) + contact.value = c + + initials.value = if (c != null) { + AppUtils.getInitials(c.name ?: "") + } else { + AppUtils.getInitials(displayName.value ?: "") + } } } abstract class GenericContactViewModel(private val sipAddress: Address) : ErrorReportingViewModel(), ContactDataInterface { - final override val contact: MutableLiveData = MutableLiveData() + final override val contact: MutableLiveData = MutableLiveData() final override val displayName: MutableLiveData = MutableLiveData() final override val securityLevel: MutableLiveData = MutableLiveData() private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactUpdated(contact: Contact) { + override fun onContactUpdated(friend: Friend) { contactLookup() } } diff --git a/app/src/main/java/org/linphone/contact/ContactLoader.kt b/app/src/main/java/org/linphone/contact/ContactLoader.kt new file mode 100644 index 000000000..689006616 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/ContactLoader.kt @@ -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 . + */ +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 { + 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() + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + 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) { + 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) { + Log.i("[Contacts Loader] Loader reset") + } +} diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt index cbc868359..db12360c2 100644 --- a/app/src/main/java/org/linphone/contact/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt @@ -23,62 +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() - @Synchronized - get - @Synchronized - private set - var sipContacts = ArrayList() - @Synchronized - get - @Synchronized - private set - val magicSearch: MagicSearch by lazy { val magicSearch = coreContext.core.createMagicSearch() magicSearch.limitedSearch = false @@ -87,42 +64,26 @@ class ContactsManager(private val context: Context) { var latestContactFetch: String = "" - private var localAccountsContacts = ArrayList() - @Synchronized - get - @Synchronized - private set + val fetchInProgress = MutableLiveData() - private val friendsMap: HashMap = HashMap() + var contactIdToWatchFor: String = "" + + private val localFriends = arrayListOf() private val contactsUpdatedListeners = ArrayList() - private var loadContactsTask: AsyncContactsLoader? = null - private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() { @Synchronized override fun onPresenceReceived(list: FriendList, friends: Array) { Log.i("[Contacts Manager] Presence received") - var sipContactsListUpdated = false for (friend in friends) { - if (refreshContactOnPresenceReceived(friend)) { - sipContactsListUpdated = true - } - } - - if (sipContactsListUpdated) { - sipContacts.sort() - Log.i("[Contacts Manager] Notifying observers that list has changed") - notifyListeners() + refreshContactOnPresenceReceived(friend) } + notifyListeners() } } init { - if (PermissionHelper.required(context).hasReadContactsPermission()) { - onReadContactsPermissionGranted() - } - initSyncAccount() val core = coreContext.core @@ -132,70 +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() { - localAccountsContacts.clear() + Log.i("[Contacts Manager] Updating local contact(s)") + 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) { - 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, sip: ArrayList) { - contacts.clear() - sipContacts.clear() - - contacts.addAll(all) - sipContacts.addAll(sip) - - updateLocalContacts() - - Log.i("[Contacts Manager] Async fetching finished, notifying observers") - notifyListeners() - } - @Synchronized fun getAndroidContactIdFromUri(uri: Uri): String? { val projection = arrayOf(ContactsContract.Data.CONTACT_ID) @@ -210,97 +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 cacheFriend = friendsMap[number] - val friend: Friend? = cacheFriend ?: coreContext.core.findFriendByPhoneNumber(number) - if (cacheFriend == null && friend != null) friendsMap[number] = friend - return friend?.userData as? Contact + fun findContactByPhoneNumber(number: String): Friend? { + return coreContext.core.findFriendByPhoneNumber(number) } @Synchronized - fun findContactByFriend(friend: Friend): 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 cacheFriend = friendsMap[cleanStringAddress] - if (cacheFriend != null) { - val contact: Contact? = cacheFriend.userData as? Contact - if (contact != null) { - Log.i("[Contacts Manager] Found contact $contact from friend in cache: $cacheFriend") - return contact - } - } - - val friends = coreContext.core.findFriends(address) - for (friend in friends) { - val contact: Contact? = friend?.userData as? Contact - if (contact != null) { - Log.i("[Contacts Manager] Found contact $contact from friend in Core: $friend") - friendsMap[cleanStringAddress] = friend - 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()) { @@ -329,31 +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() - - for (contact in sipContacts) { - contact.friend = null - } - sipContacts.clear() - val core = coreContext.core for (list in core.friendsLists) list.removeListener(friendListListener) } @@ -411,28 +259,14 @@ class ContactsManager(private val context: Context) { } @Synchronized - private fun refreshContactOnPresenceReceived(friend: Friend): Boolean { - if (friend.userData == null) return false - - val contact: Contact = friend.userData as Contact - Log.d("[Contacts Manager] Received presence information for contact $contact") + private fun refreshContactOnPresenceReceived(friend: Friend) { + 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) - } - - if (!sipContacts.contains(contact)) { - sipContacts.add(contact) - return true - } - - return false + notifyListeners(friend) } @Synchronized @@ -440,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() } @@ -476,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.avatar + ) else IconCompat.createWithAdaptiveBitmap(bm) + if (icon != null) { + personBuilder.setIcon(icon) + } + + personBuilder.setImportant(starred) + return personBuilder.build() } diff --git a/app/src/main/java/org/linphone/contact/NativeContact.kt b/app/src/main/java/org/linphone/contact/NativeContact.kt deleted file mode 100644 index 4cdb3e25a..000000000 --- a/app/src/main/java/org/linphone/contact/NativeContact.kt +++ /dev/null @@ -1,269 +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 . - */ -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 - -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.avatar - ) 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") - if (!rawPhoneNumbers.contains(number)) { - 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]" - } -} diff --git a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt index 963a86016..6e8b9c5e8 100644 --- a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt +++ b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt @@ -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 ) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index fa406e6ce..d6b95009a 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -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 @@ -56,8 +57,9 @@ import org.linphone.activities.call.IncomingCallActivity import org.linphone.activities.call.OutgoingCallActivity 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 @@ -65,7 +67,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()) @@ -87,10 +103,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) } @@ -112,7 +128,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() } } @@ -162,8 +178,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) @@ -285,6 +300,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") } @@ -313,7 +329,9 @@ class CoreContext(val context: Context, coreConfig: Config) { configureCore() + _lifecycleRegistry.currentState = Lifecycle.State.CREATED core.start() + _lifecycleRegistry.currentState = Lifecycle.State.STARTED initPhoneStateListener() @@ -331,6 +349,7 @@ class CoreContext(val context: Context, coreConfig: Config) { notificationsManager.startForeground() } + _lifecycleRegistry.currentState = Lifecycle.State.RESUMED Log.i("[Context] Started") } @@ -353,6 +372,8 @@ class CoreContext(val context: Context, coreConfig: Config) { core.removeListener(listener) stopped = true loggingService.removeListener(loggingServiceListener) + + _lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } private fun configureCore() { @@ -424,6 +445,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() { @@ -527,7 +555,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") diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index ebbf52701..7ccc2a109 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -47,7 +47,8 @@ import org.linphone.activities.call.OutgoingCallActivity import org.linphone.activities.chat_bubble.ChatBubbleActivity import org.linphone.activities.main.MainActivity 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 @@ -67,7 +68,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, @@ -423,9 +424,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 = @@ -490,9 +491,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") } @@ -627,15 +628,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) } } @@ -664,10 +665,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: String = message.contents.find { content -> content.isText }?.utf8Text ?: "" if (text.isEmpty()) { @@ -678,7 +678,7 @@ class NotificationsManager(private val context: Context) { val notifiableMessage = NotifiableMessage( text, - contact, + friend, displayName, message.time, senderAvatar = roundPicture, @@ -772,8 +772,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) { diff --git a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt index 13501d433..09a498b01 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt @@ -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 diff --git a/app/src/main/java/org/linphone/utils/ContactUtils.kt b/app/src/main/java/org/linphone/utils/ContactUtils.kt index 49a52a4ee..1c4098aa7 100644 --- a/app/src/main/java/org/linphone/utils/ContactUtils.kt +++ b/app/src/main/java/org/linphone/utils/ContactUtils.kt @@ -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 { diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 162811c99..40c7dbb0f 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -40,7 +40,8 @@ class LinphoneUtils { companion object { private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss" - fun getDisplayName(address: Address): String { + fun getDisplayName(address: Address?): String { + if (address == null) return "[null]" if (address.displayName == null) { val account = coreContext.core.accountList.find { account -> account.params.identityAddress?.asStringUriOnly() == address.asStringUriOnly() @@ -169,5 +170,16 @@ class LinphoneUtils { remoteSipUri.clean() return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}" } + + fun arePhoneNumberWeakEqual(number1: String, number2: String): Boolean { + return trimPhoneNumber(number1) == trimPhoneNumber(number2) + } + + private fun trimPhoneNumber(phoneNumber: String): String { + return phoneNumber.replace(" ", "") + .replace("-", "") + .replace("(", "") + .replace(")", "") + } } } diff --git a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt b/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt index c02c25fed..e48dac9ed 100644 --- a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt +++ b/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt @@ -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 = 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.avatar) } 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.avatar) } else { for (participant in chatRoom.participants) { diff --git a/app/src/main/res/layout-land/call_controls_fragment.xml b/app/src/main/res/layout-land/call_controls_fragment.xml index 2e955b8b8..186180418 100644 --- a/app/src/main/res/layout-land/call_controls_fragment.xml +++ b/app/src/main/res/layout-land/call_controls_fragment.xml @@ -39,7 +39,7 @@ android:orientation="vertical"> + android:text="@{data.contact.name ?? data.displayName, default=Tintin}"/> + android:text="@{data.contact.name ?? data.displayName, default=Tintin}"/> + @@ -52,6 +55,15 @@ android:singleLine="true" android:ellipsize="none" /> + + + android:visibility="@{viewModel.hasPresence() ? View.VISIBLE : View.GONE}" />