diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 627f30c56..5dab0be3a 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -326,7 +326,7 @@ class ContactLoader : LoaderManager.LoaderCallbacks { fl.updateSubscriptions() Log.i("$TAG Subscription(s) updated") - coreContext.contactsManager.onContactsLoaded() + coreContext.contactsManager.onNativeContactsLoaded() } } catch (sde: StaleDataException) { Log.e("$TAG State Data Exception: $sde") diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 5575a4911..d2b8a29ff 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -19,11 +19,17 @@ */ package org.linphone.contacts +import android.Manifest +import android.content.ContentUris import android.content.Context +import android.content.pm.PackageManager +import android.database.Cursor import android.graphics.Bitmap import android.net.Uri +import android.provider.ContactsContract import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.app.ActivityCompat import androidx.core.app.Person import androidx.core.graphics.drawable.IconCompat import androidx.loader.app.LoaderManager @@ -51,6 +57,8 @@ class ContactsManager @UiThread constructor(context: Context) { val contactAvatar: IconCompat + private var nativeContactsLoaded = false + private val listeners = arrayListOf() private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() { @@ -94,20 +102,31 @@ class ContactsManager @UiThread constructor(context: Context) { @WorkerThread fun addListener(listener: ContactsListener) { - if (coreContext.isReady()) { - listeners.add(listener) + // Post again to prevent ConcurrentModificationException + coreContext.postOnCoreThread { + try { + listeners.add(listener) + } catch (cme: ConcurrentModificationException) { + } } } @WorkerThread fun removeListener(listener: ContactsListener) { if (coreContext.isReady()) { - listeners.remove(listener) + // Post again to prevent ConcurrentModificationException + coreContext.postOnCoreThread { + try { + listeners.remove(listener) + } catch (cme: ConcurrentModificationException) { + } + } } } @UiThread - fun onContactsLoaded() { + fun onNativeContactsLoaded() { + nativeContactsLoaded = true coreContext.postOnCoreThread { notifyContactsListChanged() } @@ -131,14 +150,17 @@ class ContactsManager @UiThread constructor(context: Context) { @WorkerThread fun findContactByAddress(address: Address): Friend? { + Log.i("$TAG Looking for friend with address [${address.asStringUriOnly()}]") val username = address.username - val usernameIsPhoneNumber = !username.isNullOrEmpty() && username.startsWith("+") - return coreContext.core.findFriend(address) ?: if (usernameIsPhoneNumber) { - coreContext.core.findFriendByPhoneNumber( - username!! + val found = coreContext.core.findFriend(address) + return found ?: if (!username.isNullOrEmpty() && username.startsWith("+")) { + Log.i("$TAG Looking for friend with phone number [$username]") + val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber( + username ) + foundUsingPhoneNumber ?: findNativeContact(address) } else { - null + findNativeContact(address) } } @@ -158,6 +180,103 @@ class ContactsManager @UiThread constructor(context: Context) { } } + @WorkerThread + fun findNativeContact(address: Address): Friend? { + Log.i( + "$TAG Looking for native contact with address [${address.asStringUriOnly()}] or phone number [${address.username}]" + ) + if (nativeContactsLoaded) { + Log.w( + "$TAG Native contacts already loaded, no need to search further, no native contact matches address [${address.asStringUriOnly()}]" + ) + return null + } + + val context = coreContext.context + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + ) { + val number: String = address.username.orEmpty() + val sipUri: String = address.asStringUriOnly() + try { + val selection = "${ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER} LIKE ? OR ${ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS} LIKE ?" + val cursor: Cursor? = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + arrayOf( + ContactsContract.Data.CONTACT_ID, + ContactsContract.Contacts.LOOKUP_KEY, + ContactsContract.Data.DISPLAY_NAME_PRIMARY + ), + selection, + arrayOf(number, sipUri), + null + ) + + if (cursor != null && cursor.moveToNext()) { + val friend = coreContext.core.createFriend() + friend.edit() + + do { + val id: String = + cursor.getString( + cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID) + ) + friend.refKey = id + + if (friend.name.isNullOrEmpty()) { + val displayName: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.Data.DISPLAY_NAME_PRIMARY + ) + ) + friend.name = displayName + } + + if (friend.photo.isNullOrEmpty()) { + val picture = Uri.withAppendedPath( + ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + id.toLong() + ), + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ).toString() + friend.photo = picture + } + + if (friend.nativeUri.isNullOrEmpty()) { + val lookupKey = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.Contacts.LOOKUP_KEY + ) + ) + friend.nativeUri = + "${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey" + } + } while (cursor.moveToNext()) + + friend.address = address + friend.done() + + Log.i("$TAG Found native contact [${friend.name}] with address [$sipUri]") + cursor.close() + return friend + } + + Log.w("$TAG Failed to find native contact with address [$sipUri]") + return null + } catch (e: IllegalArgumentException) { + Log.e("$TAG Failed to search for native contact with address [$sipUri]: $e") + } + } else { + Log.w("$TAG READ_CONTACTS permission not granted, can't check native address book") + } + return null + } + @WorkerThread fun getMePerson(localAddress: Address): Person { val account = coreContext.core.accountList.find { diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 5856edbc9..5d1b41447 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -176,7 +176,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) } val chatRoomPeerAddress = chatRoom.peerAddress.asStringUriOnly() - var notifiable: Notifiable? = chatNotificationsMap[chatRoomPeerAddress] + val notifiable: Notifiable? = chatNotificationsMap[chatRoomPeerAddress] if (notifiable == null) { Log.i("$TAG No notification for chat room [$chatRoomPeerAddress], nothing to do") return @@ -489,7 +489,9 @@ class NotificationsManager @MainThread constructor(private val context: Context) Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED ) { - Log.i("$TAG Notifying [$id] with tag [$tag]") + Log.i( + "$TAG Notifying using ID [$id] and ${if (tag == null) "without tag" else "with tag [$tag]"}" + ) try { notificationManager.notify(tag, id, notification) } catch (iae: IllegalArgumentException) { @@ -509,7 +511,9 @@ class NotificationsManager @MainThread constructor(private val context: Context) @WorkerThread fun cancelNotification(id: Int, tag: String? = null) { - Log.i("$TAG Canceling [$id] with tag [$tag]") + Log.i( + "$TAG Canceling notification with ID [$id] and ${if (tag == null) "without tag" else "with tag [$tag]"}" + ) notificationManager.cancel(tag, id) }