From a2d038eb46bebf2200332a3ab91617e94346286e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 22 Jun 2023 17:35:17 +0200 Subject: [PATCH] Fixes & improvements --- app/build.gradle | 2 - app/src/main/AndroidManifest.xml | 3 + .../org/linphone/contacts/ContactLoader.kt | 275 ++++++++++++++++++ .../main/java/org/linphone/ui/MainActivity.kt | 33 +++ .../linphone/ui/conversations/ChatRoomData.kt | 40 +-- .../conversations/ConversationsListAdapter.kt | 3 +- .../ConversationsListViewModel.kt | 91 +++++- .../org/linphone/utils/PhoneNumberUtils.kt | 98 +++++++ 8 files changed, 515 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/org/linphone/contacts/ContactLoader.kt create mode 100644 app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 7d1138440..db01576d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,8 +56,6 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.3.1-rc01' - implementation 'androidx.core:core-ktx:+' - implementation 'androidx.core:core-ktx:+' def nav_version = "2.6.0" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f104b6252..e7355b719 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + . + */ +package org.linphone.contacts + +import android.content.ContentUris +import android.database.Cursor +import android.database.StaleDataException +import android.net.Uri +import android.os.Bundle +import android.provider.ContactsContract +import android.util.Patterns +import androidx.loader.app.LoaderManager +import androidx.loader.content.CursorLoader +import androidx.loader.content.Loader +import java.lang.Exception +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.PhoneNumberUtils + +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, + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.TYPE, + ContactsContract.CommonDataKinds.Phone.LABEL, + ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER + ) + + const val linphoneMime = "vnd.android.cursor.item/vnd.org.linphone.provider.sip_address" + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + val mimeType = ContactsContract.Data.MIMETYPE + val mimeSelection = "$mimeType = ? OR $mimeType = ? OR $mimeType = ?" + + val selection = ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1 AND ($mimeSelection)" + val selectionArgs = arrayOf( + linphoneMime, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + ) + + return CursorLoader( + coreContext.context, + ContactsContract.Data.CONTENT_URI, + projection, + selection, + selectionArgs, + ContactsContract.Data.CONTACT_ID + " ASC" + ) + } + + override fun onLoadFinished(loader: Loader, cursor: Cursor?) { + if (cursor == null) { + Log.e("[Contacts Loader] Cursor is null!") + return + } + Log.i("[Contacts Loader] Load finished, found ${cursor.count} entries in cursor") + + val core = coreContext.core + if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { + Log.w("[Contacts Loader] Core is being stopped or already destroyed, abort") + return + } + + coreContext.postOnCoreThread { core -> + val friends = HashMap() + + try { + // Cursor can be null now that we are on a different dispatcher according to Crashlytics + val friendsPhoneNumbers = arrayListOf() + val friendsAddresses = arrayListOf
() + var previousId = "" + while (cursor != null && !cursor.isClosed && cursor.moveToNext()) { + try { + val id: String = + cursor.getString( + cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID) + ) + val mime: String? = + cursor.getString( + cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE) + ) + + if (previousId.isEmpty() || previousId != id) { + friendsPhoneNumbers.clear() + friendsAddresses.clear() + previousId = id + } + + val friend = friends[id] ?: core.createFriend() + friend.refKey = id + if (friend.name.isNullOrEmpty()) { + val displayName: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.Data.DISPLAY_NAME_PRIMARY + ) + ) + friend.name = displayName + + friend.photo = Uri.withAppendedPath( + ContentUris.withAppendedId( + ContactsContract.Contacts.CONTENT_URI, + id.toLong() + ), + ContactsContract.Contacts.Photo.CONTENT_DIRECTORY + ).toString() + + val starred = + cursor.getInt( + cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED) + ) == 1 + friend.starred = starred + val lookupKey = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.Contacts.LOOKUP_KEY + ) + ) + friend.nativeUri = + "${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey" + + // Disable short term presence + friend.isSubscribesEnabled = false + friend.incSubscribePolicy = SubscribePolicy.SPDeny + } + + when (mime) { + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { + val data1: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.NUMBER + ) + ) + val data2: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.TYPE + ) + ) + val data3: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.LABEL + ) + ) + val data4: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER + ) + ) + + val label = + PhoneNumberUtils.addressBookLabelTypeToVcardParamString( + data2?.toInt() + ?: ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM, + data3 + ) + + val number = + if (data1.isNullOrEmpty() || + !Patterns.PHONE.matcher(data1).matches() + ) { + data4 ?: data1 + } else { + data1 + } + + if (number != null) { + if ( + friendsPhoneNumbers.find { + PhoneNumberUtils.arePhoneNumberWeakEqual( + it, + number + ) + } == null + ) { + val phoneNumber = Factory.instance() + .createFriendPhoneNumber(number, label) + friend.addPhoneNumberWithLabel(phoneNumber) + friendsPhoneNumbers.add(number) + } + } + } + linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> { + val sipAddress: String? = + cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS + ) + ) + if (sipAddress != null) { + val address = core.interpretUrl(sipAddress, true) + if (address != null && + friendsAddresses.find { + it.weakEqual(address) + } == null + ) { + friend.addAddress(address) + friendsAddresses.add(address) + } + } + } + } + + friends[id] = friend + } catch (e: Exception) { + Log.e("[Contacts Loader] Exception: $e") + } + } + + if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { + Log.w("[Contacts Loader] Core is being stopped or already destroyed, abort") + } else if (friends.isEmpty()) { + Log.w("[Contacts Loader] No friend created!") + } else { + Log.i("[Contacts Loader] ${friends.size} friends created") + + val fl = core.defaultFriendList ?: core.createFriendList() + for (friend in fl.friends) { + fl.removeFriend(friend) + } + + if (fl != core.defaultFriendList) core.addFriendList(fl) + + val friendsList = friends.values + for (friend in friendsList) { + fl.addLocalFriend(friend) + } + friends.clear() + Log.i("[Contacts Loader] Friends added") + + fl.updateSubscriptions() + Log.i("[Contacts Loader] Subscription(s) updated") + } + } catch (sde: StaleDataException) { + Log.e("[Contacts Loader] State Data Exception: $sde") + } catch (ise: IllegalStateException) { + Log.e("[Contacts Loader] Illegal State Exception: $ise") + } catch (e: Exception) { + Log.e("[Contacts Loader] Exception: $e") + } + } + } + + override fun onLoaderReset(loader: Loader) { + Log.i("[Contacts Loader] Loader reset") + } +} diff --git a/app/src/main/java/org/linphone/ui/MainActivity.kt b/app/src/main/java/org/linphone/ui/MainActivity.kt index 0048d8c24..59967e4b7 100644 --- a/app/src/main/java/org/linphone/ui/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/MainActivity.kt @@ -19,21 +19,29 @@ */ package org.linphone.ui +import android.Manifest +import android.content.pm.PackageManager import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import androidx.loader.app.LoaderManager import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.navigation.NavigationBarView import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.contacts.ContactLoader import org.linphone.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { + companion object { + private const val CONTACTS_PERMISSION_REQUEST = 0 + } + private lateinit var binding: ActivityMainBinding private lateinit var viewModel: MainViewModel @@ -46,6 +54,11 @@ class MainActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, true) super.onCreate(savedInstanceState) + if (checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { + val manager = LoaderManager.getInstance(this) + manager.restartLoader(0, null, ContactLoader()) + } + while (!coreContext.isReady()) { Thread.sleep(20) } @@ -75,6 +88,26 @@ class MainActivity : AppCompatActivity() { .addOnDestinationChangedListener(onNavDestinationChangedListener) getNavBar()?.setupWithNavController(binding.mainNavHostFragment.findNavController()) + + if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + arrayOf(Manifest.permission.READ_CONTACTS), + CONTACTS_PERMISSION_REQUEST + ) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == CONTACTS_PERMISSION_REQUEST && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + val manager = LoaderManager.getInstance(this) + manager.restartLoader(0, null, ContactLoader()) + } + + super.onRequestPermissionsResult(requestCode, permissions, grantResults) } private fun getNavBar(): NavigationBarView? { diff --git a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt index 2465148c9..e5bcb19dd 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt @@ -92,6 +92,27 @@ class ChatRoomData(val chatRoom: ChatRoom) { } init { + coreContext.postOnCoreThread { core -> + chatRoom.addListener(chatRoomListener) + } + } + + fun onCleared() { + coreContext.postOnCoreThread { core -> + chatRoom.removeListener(chatRoomListener) + } + } + + fun onClicked() { + chatRoomDataListener?.onClicked() + } + + fun onLongClicked(): Boolean { + chatRoomDataListener?.onLongClicked() + return true + } + + fun update() { if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { val remoteAddress = chatRoom.peerAddress val friend = chatRoom.core.findFriend(remoteAddress) @@ -122,25 +143,6 @@ class ChatRoomData(val chatRoom: ChatRoom) { isSecureVerified.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Safe) isEphemeral.postValue(chatRoom.isEphemeralEnabled) isMuted.postValue(areNotificationsMuted()) - - coreContext.postOnCoreThread { core -> - chatRoom.addListener(chatRoomListener) - } - } - - fun onCleared() { - coreContext.postOnCoreThread { core -> - chatRoom.removeListener(chatRoomListener) - } - } - - fun onClicked() { - chatRoomDataListener?.onClicked() - } - - fun onLongClicked(): Boolean { - chatRoomDataListener?.onLongClicked() - return true } private fun computeLastMessageImdnIcon(message: ChatMessage) { diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt index 425541da9..8f4e22a58 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt @@ -68,6 +68,7 @@ class ConversationsListAdapter( ) : RecyclerView.ViewHolder(binding.root) { fun bind(chatRoomData: ChatRoomData) { with(binding) { + chatRoomData.update() data = chatRoomData lifecycleOwner = viewLifecycleOwner @@ -93,7 +94,7 @@ class ConversationsListAdapter( private class ConversationDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean { - return oldItem.id.compareTo(newItem.id) == 0 + return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean { diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt index 5c128b4d0..ccb6ac77d 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt @@ -27,7 +27,9 @@ import org.linphone.core.ChatMessage import org.linphone.core.ChatRoom import org.linphone.core.Core import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils class ConversationsListViewModel : ViewModel() { val chatRoomsList = MutableLiveData>() @@ -40,25 +42,42 @@ class ConversationsListViewModel : ViewModel() { chatRoom: ChatRoom, state: ChatRoom.State? ) { - if (state == ChatRoom.State.Created || state == ChatRoom.State.Instantiated || state == ChatRoom.State.Deleted) { - updateChatRoomsList() + Log.i( + "[Conversations List] Chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]" + ) + when (state) { + ChatRoom.State.Created -> { + addChatRoomToList(chatRoom) + } + ChatRoom.State.Deleted -> { + removeChatRoomFromList(chatRoom) + } + else -> {} } } - override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { - updateChatRoomsList() + override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + onChatRoomMessageEvent(chatRoom) } override fun onMessagesReceived( core: Core, - room: ChatRoom, + chatRoom: ChatRoom, messages: Array ) { - reorderChatRoomsList() + onChatRoomMessageEvent(chatRoom) } - override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) { - reorderChatRoomsList() + override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { + notifyChatRoomUpdate(chatRoom) + } + + override fun onChatRoomEphemeralMessageDeleted(core: Core, chatRoom: ChatRoom) { + notifyChatRoomUpdate(chatRoom) + } + + override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) { + notifyChatRoomUpdate(chatRoom) } } @@ -76,7 +95,62 @@ class ConversationsListViewModel : ViewModel() { super.onCleared() } + private fun addChatRoomToList(chatRoom: ChatRoom) { + coreContext.postOnCoreThread { core -> + val list = arrayListOf() + + val data = ChatRoomData(chatRoom) + list.add(data) + list.addAll(chatRoomsList.value.orEmpty()) + + chatRoomsList.postValue(list) + } + } + + private fun removeChatRoomFromList(chatRoom: ChatRoom) { + coreContext.postOnCoreThread { core -> + val list = arrayListOf() + + for (data in chatRoomsList.value.orEmpty()) { + if (LinphoneUtils.getChatRoomId(chatRoom) != LinphoneUtils.getChatRoomId( + data.chatRoom + ) + ) { + list.add(data) + } + } + + chatRoomsList.postValue(list) + } + } + + private fun findChatRoomIndex(chatRoom: ChatRoom): Int { + val id = LinphoneUtils.getChatRoomId(chatRoom) + for ((index, data) in chatRoomsList.value.orEmpty().withIndex()) { + if (id == data.id) { + return index + } + } + return -1 + } + + private fun notifyChatRoomUpdate(chatRoom: ChatRoom) { + when (val index = findChatRoomIndex(chatRoom)) { + -1 -> updateChatRoomsList() + else -> notifyItemChangedEvent.postValue(Event(index)) + } + } + + private fun onChatRoomMessageEvent(chatRoom: ChatRoom) { + when (findChatRoomIndex(chatRoom)) { + -1 -> updateChatRoomsList() + 0 -> notifyItemChangedEvent.postValue(Event(0)) + else -> reorderChatRoomsList() + } + } + private fun updateChatRoomsList() { + Log.i("[Conversations List] Updating chat rooms list") coreContext.postOnCoreThread { core -> chatRoomsList.value.orEmpty().forEach(ChatRoomData::onCleared) @@ -90,6 +164,7 @@ class ConversationsListViewModel : ViewModel() { } private fun reorderChatRoomsList() { + Log.i("[Conversations List] Re-ordering chat rooms list") coreContext.postOnCoreThread { core -> val list = arrayListOf() list.addAll(chatRoomsList.value.orEmpty()) diff --git a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt new file mode 100644 index 000000000..6623d12a2 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010-2023 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.utils + +import android.content.res.Resources +import android.provider.ContactsContract + +class PhoneNumberUtils { + companion object { + fun addressBookLabelTypeToVcardParamString(type: Int, default: String?): String { + return when (type) { + ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT -> "assistant" + ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK -> "callback" + ContactsContract.CommonDataKinds.Phone.TYPE_CAR -> "car" + ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN -> "work,main" + ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME -> "home,fax" + ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK -> "work,fax" + ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "home" + ContactsContract.CommonDataKinds.Phone.TYPE_ISDN -> "isdn" + ContactsContract.CommonDataKinds.Phone.TYPE_MAIN -> "main" + ContactsContract.CommonDataKinds.Phone.TYPE_MMS -> "text" + ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "cell" + ContactsContract.CommonDataKinds.Phone.TYPE_OTHER -> "other" + ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX -> "fax" + ContactsContract.CommonDataKinds.Phone.TYPE_PAGER -> "pager" + ContactsContract.CommonDataKinds.Phone.TYPE_RADIO -> "radio" + ContactsContract.CommonDataKinds.Phone.TYPE_TELEX -> "telex" + ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD -> "textphone" + ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "work" + ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE -> "work,cell" + ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER -> "work,pager" + ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM -> default ?: "custom" + else -> default ?: type.toString() + } + } + + fun vcardParamStringToAddressBookLabel(resources: Resources, label: String): String { + if (label.isEmpty()) return label + val type = labelToType(label) + return ContactsContract.CommonDataKinds.Phone.getTypeLabel(resources, type, label).toString() + } + + private fun labelToType(label: String): Int { + return when (label) { + "assistant" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT + "callback" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK + "car" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR + "work,main" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN + "home,fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME + "work,fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK + "home" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME + "isdn" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN + "main" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN + "text" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS + "cell" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE + "other" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER + "fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX + "pager" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER + "radio" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO + "telex" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX + "textphone" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD + "work" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK + "work,cell" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE + "work,pager" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER + "custom" -> ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM + else -> ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM + } + } + + fun arePhoneNumberWeakEqual(number1: String, number2: String): Boolean { + return trimPhoneNumber(number1) == trimPhoneNumber(number2) + } + + private fun trimPhoneNumber(phoneNumber: String): String { + return phoneNumber.replace(" ", "") + .replace("-", "") + .replace("(", "") + .replace(")", "") + } + } +}