diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 65c8521ef..cf926707a 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -52,6 +52,8 @@ import org.linphone.core.Factory import org.linphone.core.Friend import org.linphone.core.FriendList import org.linphone.core.FriendListListenerStub +import org.linphone.core.MagicSearch +import org.linphone.core.MagicSearchListenerStub import org.linphone.core.SecurityLevel import org.linphone.core.tools.Log import org.linphone.ui.main.MainActivity @@ -70,7 +72,8 @@ class ContactsManager @UiThread constructor() { private const val TAG = "[Contacts Manager]" private const val DELAY_BEFORE_RELOADING_CONTACTS_AFTER_PRESENCE_RECEIVED = 1000L // 1 second - private const val FRIEND_LIST_TEMPORARY_STORED = "TempNativeContacts" + private const val FRIEND_LIST_TEMPORARY_STORED_NATIVE = "TempNativeContacts" + private const val FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY = "TempRemoteDirectoryContacts" } private var nativeContactsLoaded = false @@ -88,6 +91,36 @@ class ContactsManager @UiThread constructor() { private var loadContactsOnlyFromDefaultDirectory = true + private lateinit var magicSearch: MagicSearch + + private val magicSearchListener = object : MagicSearchListenerStub() { + @WorkerThread + override fun onSearchResultsReceived(magicSearch: MagicSearch) { + val results = magicSearch.lastSearch + Log.i("$TAG [${results.size}] magic search results available") + + if (results.isNotEmpty()) { + val result = results.first() { + it.friend != null + } + if (result != null) { + val friend = result.friend!! + Log.i("$TAG Found matching friend in source [${result.sourceFlags}]") + + // Store friend in app's cache to be re-used in call history, conversations, etc... + val temporaryFriendList = getTemporaryFriendList(native = false) + temporaryFriendList.addFriend(friend) + newContactAdded(friend) + Log.i("$TAG Stored discovered friend in temporary friend list, for later use") + + for (listener in listeners) { + listener.onContactFoundInRemoteDirectory(friend) + } + } + } + } + } + private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() { @WorkerThread override fun onNewSipAddressDiscovered( @@ -106,7 +139,7 @@ class ContactsManager @UiThread constructor() { friend.addAddress(address) friend.done() - newContactAddedWithSipUri(sipUri) + newContactAddedWithSipUri(friend, sipUri) } else { Log.e("$TAG Failed to parse SIP URI [$sipUri] as Address!") } @@ -182,7 +215,7 @@ class ContactsManager @UiThread constructor() { } @WorkerThread - private fun newContactAddedWithSipUri(sipUri: String) { + private fun newContactAddedWithSipUri(friend: Friend, sipUri: String) { if (unknownContactsAvatarsMap.keys.contains(sipUri)) { Log.d("$TAG Found SIP URI [$sipUri] in unknownContactsAvatarsMap, removing it") val oldModel = unknownContactsAvatarsMap[sipUri] @@ -195,13 +228,19 @@ class ContactsManager @UiThread constructor() { val oldModel = knownContactsAvatarsMap[sipUri] val address = Factory.instance().createAddress(sipUri) oldModel?.update(address) + } else { + Log.i( + "$TAG New contact added with SIP URI [$sipUri] but no avatar yet, let's create it" + ) + val model = ContactAvatarModel(friend) + knownContactsAvatarsMap[sipUri] = model } } @WorkerThread fun newContactAdded(friend: Friend) { for (sipAddress in friend.addresses) { - newContactAddedWithSipUri(sipAddress.asStringUriOnly()) + newContactAddedWithSipUri(friend, sipAddress.asStringUriOnly()) } conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy) @@ -240,14 +279,12 @@ class ContactsManager @UiThread constructor() { Log.i("$TAG Native contacts have been loaded, cleaning avatars maps") val core = coreContext.core - val found = core.getFriendListByName(FRIEND_LIST_TEMPORARY_STORED) - if (found != null) { - val count = found.friends.size - Log.i( - "$TAG Found temporary friend list with [$count] friends, removing it as no longer necessary" - ) - core.removeFriendList(found) - } + val found = getTemporaryFriendList(native = true) + val count = found.friends.size + Log.i( + "$TAG Found temporary friend list with [$count] friends, removing it as no longer necessary" + ) + core.removeFriendList(found) knownContactsAvatarsMap.values.forEach(ContactAvatarModel::destroy) knownContactsAvatarsMap.clear() @@ -296,6 +333,21 @@ class ContactsManager @UiThread constructor() { return found } + // Start an async query in Magic Search in case LDAP or remote CardDAV is configured + val remoteContactDirectories = coreContext.core.remoteContactDirectories + if (remoteContactDirectories.isNotEmpty()) { + Log.i( + "$TAG SIP URI [$sipUri] not found in locally stored Friends, trying LDAP/CardDAV remote directory" + ) + magicSearch.resetSearchCache() + magicSearch.getContactsListAsync( + username, + address.domain, + MagicSearch.Source.LdapServers.toInt() or MagicSearch.Source.RemoteCardDAV.toInt(), + MagicSearch.Aggregation.Friend + ) + } + val sipAddress = if (sipUri.startsWith("sip:")) { sipUri.substring("sip:".length) } else if (sipUri.startsWith("sips:")) { @@ -435,6 +487,17 @@ class ContactsManager @UiThread constructor() { return avatar } + @WorkerThread + fun isContactAvailable(friend: Friend): Boolean { + return !friend.refKey.isNullOrEmpty() && !isContactTemporary(friend) + } + + @WorkerThread + fun isContactTemporary(friend: Friend): Boolean { + val friendList = friend.friendList + return friendList == null || friendList.displayName == FRIEND_LIST_TEMPORARY_STORED_NATIVE || friendList.displayName == FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY + } + @WorkerThread fun onCoreStarted(core: Core) { loadContactsOnlyFromDefaultDirectory = corePreferences.fetchContactsFromDefaultDirectory @@ -444,6 +507,10 @@ class ContactsManager @UiThread constructor() { list.addListener(friendListListener) } + magicSearch = core.createMagicSearch() + magicSearch.limitedSearch = false + magicSearch.addListener(magicSearchListener) + val context = coreContext.context if (ActivityCompat.checkSelfPermission( context, @@ -467,12 +534,31 @@ class ContactsManager @UiThread constructor() { @WorkerThread fun onCoreStopped(core: Core) { coroutineScope.cancel() + + magicSearch.removeListener(magicSearchListener) core.removeListener(coreListener) + for (list in core.friendsLists) { list.removeListener(friendListListener) } } + @WorkerThread + fun getTemporaryFriendList(native: Boolean): FriendList { + val core = coreContext.core + val name = if (native) FRIEND_LIST_TEMPORARY_STORED_NATIVE else FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY + val temporaryFriendList = core.getFriendListByName(name) ?: core.createFriendList() + if (temporaryFriendList.displayName.isNullOrEmpty()) { + temporaryFriendList.isDatabaseStorageEnabled = false + temporaryFriendList.displayName = name + core.addFriendList(temporaryFriendList) + Log.i( + "$TAG Created temporary friend list with name [$name]" + ) + } + return temporaryFriendList + } + @WorkerThread fun findNativeContact(address: String, username: String, searchAsPhoneNumber: Boolean): Friend? { if (nativeContactsLoaded) { @@ -499,16 +585,7 @@ class ContactsManager @UiThread constructor() { "$TAG Looking for native contact with address [$address] ${if (searchAsPhoneNumber) "or phone number [$username]" else ""}" ) - val temporaryFriendList = core.getFriendListByName(FRIEND_LIST_TEMPORARY_STORED) ?: core.createFriendList() - if (temporaryFriendList.displayName.isNullOrEmpty()) { - temporaryFriendList.isDatabaseStorageEnabled = false - temporaryFriendList.displayName = FRIEND_LIST_TEMPORARY_STORED - core.addFriendList(temporaryFriendList) - Log.i( - "$TAG Created temporary friend list with name [$FRIEND_LIST_TEMPORARY_STORED]" - ) - } - + val temporaryFriendList = getTemporaryFriendList(native = true) try { val selection = if (searchAsPhoneNumber) { "${ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER} LIKE ? OR ${ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS} LIKE ? OR ${ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS} LIKE ? OR ${ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS} LIKE ?" @@ -649,6 +726,8 @@ class ContactsManager @UiThread constructor() { interface ContactsListener { fun onContactsLoaded() + + fun onContactFoundInRemoteDirectory(friend: Friend) } } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 700efd1fb..54c282900 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -50,6 +50,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.compatibility.Compatibility import org.linphone.contacts.AvatarGenerator +import org.linphone.contacts.ContactsManager.ContactsListener import org.linphone.contacts.getAvatarBitmap import org.linphone.contacts.getPerson import org.linphone.core.Address @@ -59,10 +60,12 @@ import org.linphone.core.ChatMessageListener import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatMessageReaction import org.linphone.core.ChatRoom +import org.linphone.core.ConferenceParams import org.linphone.core.Core import org.linphone.core.CoreInCallService import org.linphone.core.CoreKeepAliveThirdPartyAccountsService import org.linphone.core.CoreListenerStub +import org.linphone.core.Factory import org.linphone.core.Friend import org.linphone.core.MediaDirection import org.linphone.core.tools.Log @@ -116,6 +119,57 @@ class NotificationsManager @MainThread constructor(private val context: Context) private var currentlyDisplayedChatRoomId: String = "" + private val contactsListener = object : ContactsListener { + @WorkerThread + override fun onContactsLoaded() { } + + @WorkerThread + override fun onContactFoundInRemoteDirectory(friend: Friend) { + val addresses = friend.addresses + Log.i( + "$TAG Found contact [${friend.name}] in remote directory with [${addresses.size}] addresses" + ) + + for ((remoteAddress, notifiable) in callNotificationsMap.entries) { + val parsedAddress = Factory.instance().createAddress(remoteAddress) + parsedAddress ?: continue + val addressMatch = addresses.find { + it.weakEqual(parsedAddress) + } + if (addressMatch != null) { + Log.i( + "$TAG Found call [${addressMatch.asStringUriOnly()}] with contact in notifications, updating it" + ) + updateCallNotification(notifiable, addressMatch, friend) + } + } + + for ((remoteAddress, notifiable) in chatNotificationsMap.entries) { + var updated = false + var peerAddress: Address? = null + for (message in notifiable.messages) { + val parsedAddress = Factory.instance().createAddress(message.senderAddress) + parsedAddress ?: continue + val addressMatch = addresses.find { + it.weakEqual(parsedAddress) + } + if (addressMatch != null) { + peerAddress = addressMatch + updated = true + message.sender = friend.name ?: LinphoneUtils.getDisplayName(addressMatch) + message.friend = friend + } + } + if (updated && peerAddress != null) { + Log.i( + "$TAG Found conversation [${peerAddress.asStringUriOnly()}] with contact in notifications, updating it" + ) + updateConversationNotification(notifiable, peerAddress) + } + } + } + } + private val coreListener = object : CoreListenerStub() { @WorkerThread override fun onCallStateChanged( @@ -468,12 +522,15 @@ class NotificationsManager @MainThread constructor(private val context: Context) } core.addListener(coreListener) + + coreContext.contactsManager.addListener(contactsListener) } @WorkerThread fun onCoreStopped(core: Core) { Log.i("$TAG Getting destroyed, clearing foreground Service & call notifications") core.removeListener(coreListener) + coreContext.contactsManager.removeListener(contactsListener) } @WorkerThread @@ -499,7 +556,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) } @WorkerThread - private fun showCallNotification(call: Call, isIncoming: Boolean) { + private fun showCallNotification(call: Call, isIncoming: Boolean, friend: Friend? = null) { val notifiable = getNotifiableForCall(call) val callNotificationIntent = Intent(context, CallActivity::class.java) @@ -518,11 +575,11 @@ class NotificationsManager @MainThread constructor(private val context: Context) ) val notification = createCallNotification( - context, call, notifiable, pendingIntent, - isIncoming + isIncoming, + friend ) if (isIncoming) { currentlyRingingCallRemoteAddress = call.remoteAddress @@ -801,6 +858,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) text, contact, displayName, + address.asStringUriOnly(), message.time * 1000, /* Linphone timestamps are in seconds */ isOutgoing = false, isReaction = true, @@ -826,6 +884,33 @@ class NotificationsManager @MainThread constructor(private val context: Context) } } + @WorkerThread + private fun updateConversationNotification(notifiable: Notifiable, remoteAddress: Address) { + val localAddress = Factory.instance().createAddress(notifiable.localIdentity.orEmpty()) + localAddress ?: return + val params: ConferenceParams? = null + val chatRoom: ChatRoom? = coreContext.core.searchChatRoom( + params, + localAddress, + remoteAddress, + arrayOfNulls
(0) + ) + chatRoom ?: return + + val me = coreContext.contactsManager.getMePerson(chatRoom.localAddress) + val pendingIntent = getChatRoomPendingIntent(chatRoom, notifiable.notificationId) + val notification = createMessageNotification( + notifiable, + pendingIntent, + LinphoneUtils.getChatRoomId(chatRoom), + me + ) + Log.i( + "$TAG Updating chat notification with ID [${notifiable.notificationId}]" + ) + notify(notifiable.notificationId, notification, CHAT_TAG) + } + @WorkerThread private fun notify(id: Int, notification: Notification, tag: String? = null) { if (ActivityCompat.checkSelfPermission( @@ -876,7 +961,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) var notifiable: Notifiable? = callNotificationsMap[address] if (notifiable == null) { notifiable = Notifiable(getNotificationIdForCall(call)) - notifiable.remoteAddress = call.remoteAddress.asStringUriOnly() + notifiable.remoteAddress = address callNotificationsMap[address] = notifiable } @@ -890,10 +975,12 @@ class NotificationsManager @MainThread constructor(private val context: Context) val displayName = contact?.name ?: LinphoneUtils.getDisplayName(message.fromAddress) val text = LinphoneUtils.getPlainTextDescribingMessage(message) + val address = message.fromAddress val notifiableMessage = NotifiableMessage( text, contact, displayName, + address.asStringUriOnly(), message.time * 1000, /* Linphone timestamps are in seconds */ isOutgoing = message.isOutgoing ) @@ -922,11 +1009,11 @@ class NotificationsManager @MainThread constructor(private val context: Context) @WorkerThread private fun createCallNotification( - context: Context, call: Call, notifiable: Notifiable, pendingIntent: PendingIntent?, - isIncoming: Boolean + isIncoming: Boolean, + friend: Friend? = null ): Notification { val declineIntent = getCallDeclinePendingIntent(notifiable) val answerIntent = getCallAnswerPendingIntent(notifiable) @@ -953,8 +1040,8 @@ class NotificationsManager @MainThread constructor(private val context: Context) .setImportant(false) .build() } else { - val contact = - coreContext.contactsManager.findContactByAddress(remoteAddress) + val contact = friend + ?: coreContext.contactsManager.findContactByAddress(remoteAddress) val displayName = contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) getPerson(contact, displayName) @@ -1036,6 +1123,47 @@ class NotificationsManager @MainThread constructor(private val context: Context) return builder.build() } + @WorkerThread + private fun updateCallNotification( + notifiable: Notifiable, + remoteAddress: Address, + friend: Friend + ) { + val call = coreContext.core.getCallByRemoteAddress2(remoteAddress) + if (call == null) { + Log.w( + "$TAG Failed to find call with remote SIP URI [${remoteAddress.asStringUriOnly()}]" + ) + return + } + val isIncoming = LinphoneUtils.isCallIncoming(call.state) + + val notification = notificationsMap[notifiable.notificationId] + if (notification == null) { + Log.w( + "$TAG Failed to find notification with ID [${notifiable.notificationId}], creating a new one" + ) + showCallNotification(call, isIncoming, friend) + return + } + + val pendingIntent = notification.fullScreenIntent + val newNotification = createCallNotification( + call, + notifiable, + pendingIntent, + isIncoming, + friend + ) + if (isIncoming) { + Log.i("$TAG Updating incoming call notification with ID [$INCOMING_CALL_ID]") + notify(INCOMING_CALL_ID, newNotification) + } else { + Log.i("$TAG Updating call notification with ID [${notifiable.notificationId}]") + notify(notifiable.notificationId, newNotification) + } + } + @WorkerThread private fun createMessageNotification( notifiable: Notifiable, @@ -1078,6 +1206,9 @@ class NotificationsManager @MainThread constructor(private val context: Context) style.conversationTitle = if (notifiable.isGroup) notifiable.groupTitle else lastPerson?.name style.isGroupConversation = notifiable.isGroup + Log.i( + "$TAG Conversation is ${if (style.isGroupConversation) "group" else "1-1"} with title [${style.conversationTitle}]" + ) val largeIcon = lastPersonAvatar val notificationBuilder = NotificationCompat.Builder( @@ -1131,7 +1262,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) val notifiable: Notifiable? = chatNotificationsMap[address] if (notifiable != null) { Log.i( - "$TAG Dismissing notification for conversation $chatRoom with id ${notifiable.notificationId}" + "$TAG Dismissing notification for conversation [${chatRoom.peerAddress.asStringUriOnly()}] with id ${notifiable.notificationId}" ) notifiable.messages.clear() cancelNotification(notifiable.notificationId, CHAT_TAG) @@ -1193,6 +1324,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) text, null, notifiable.myself ?: LinphoneUtils.getDisplayName(senderAddress), + senderAddress.asStringUriOnly(), System.currentTimeMillis(), isOutgoing = true ) @@ -1459,9 +1591,10 @@ class NotificationsManager @MainThread constructor(private val context: Context) } class NotifiableMessage( - var message: String, - val friend: Friend?, - val sender: String, + val message: String, + var friend: Friend?, + var sender: String, + val senderAddress: String, val time: Long, var filePath: Uri? = null, var fileMime: String? = null, diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index a4c7dff9d..16dde38b4 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -349,8 +349,8 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { val isAdmin = participantModel.isParticipantAdmin popupView.isParticipantAdmin = isAdmin popupView.isMeAdmin = participantModel.isMyselfAdmin - val friendRefKey = participantModel.avatarModel.friend.refKey - popupView.isParticipantContact = !friendRefKey.isNullOrEmpty() + val friendRefKey = participantModel.refKey + popupView.isParticipantContact = participantModel.friendAvailable popupView.setRemoveParticipantClickListener { Log.i("$TAG Trying to remove participant [$address]") @@ -372,7 +372,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { popupView.setSeeContactProfileClickListener { Log.i("$TAG Trying to display participant [$address] contact page") - if (!friendRefKey.isNullOrEmpty()) { + if (friendRefKey.isNotEmpty()) { sharedViewModel.navigateToContactsEvent.value = Event(true) sharedViewModel.showContactEvent.value = Event(friendRefKey) } else { diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt index c966dc074..ae6ecef86 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt @@ -24,6 +24,7 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Address +import org.linphone.ui.main.contacts.model.ContactAvatarModel class ParticipantModel @WorkerThread constructor( val address: Address, @@ -36,7 +37,15 @@ class ParticipantModel @WorkerThread constructor( ) { val sipUri = address.asStringUriOnly() - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address) + val avatarModel: ContactAvatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + address + ) + + val refKey: String = avatarModel.friend.refKey.orEmpty() + + val friendAvailable: Boolean = coreContext.contactsManager.isContactAvailable( + avatarModel.friend + ) @UiThread fun onClicked() { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index d8b816dd7..069494c74 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -76,6 +76,8 @@ class ConversationInfoViewModel @UiThread constructor() : AbstractConversationVi val oneToOneParticipantRefKey = MutableLiveData