Fixed issue with contact editor since we switched to Friend as original value from addressbook will be lost in Friend causing edition/removal in native addressbook to silently fail

This commit is contained in:
Sylvain Berfini 2022-11-21 13:55:03 +01:00
parent 11f36dcb63
commit ee40995f34
5 changed files with 103 additions and 61 deletions

View file

@ -43,6 +43,7 @@ Group changes to describe their impact on the project, as follows:
- Show service notification sooner to prevent crash if Core creation takes too long - Show service notification sooner to prevent crash if Core creation takes too long
- Incoming call screen not being showed up to user (& screen staying off) when using app in Samsung secure folder - Incoming call screen not being showed up to user (& screen staying off) when using app in Samsung secure folder
- One to one chat room creation process waiting indefinitely if chat room already exists - One to one chat room creation process waiting indefinitely if chat room already exists
- Contact edition (SIP addresses & phone numbers) not working due to original value being lost in Friend parsing
- "Blinking" in some views when presence is being received - "Blinking" in some views when presence is being received
- Trying to keep the preferred driver (OpenSLES / AAudio) when switching device - Trying to keep the preferred driver (OpenSLES / AAudio) when switching device
- Issues when storing presence in native contacts + potentially duplicated SIP addresses in contact details - Issues when storing presence in native contacts + potentially duplicated SIP addresses in contact details

View file

@ -506,13 +506,15 @@ internal fun MasterContactsFragment.clearDisplayedContact() {
} }
internal fun ContactEditorFragment.navigateToContact(id: String) { internal fun ContactEditorFragment.navigateToContact(id: String) {
val bundle = Bundle() if (findNavController().currentDestination?.id == R.id.contactEditorFragment) {
bundle.putString("id", id) val bundle = Bundle()
findNavController().navigate( bundle.putString("id", id)
R.id.action_contactEditorFragment_to_detailContactFragment, findNavController().navigate(
bundle, R.id.action_contactEditorFragment_to_detailContactFragment,
popupTo(R.id.contactEditorFragment, true) bundle,
) popupTo(R.id.contactEditorFragment, true)
)
}
} }
internal fun DetailContactFragment.navigateToChatRoom(args: Bundle?) { internal fun DetailContactFragment.navigateToChatRoom(args: Bundle?) {

View file

@ -17,43 +17,32 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.linphone.activities.main.contact.viewmodels package org.linphone.activities.main.contact.data
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.ExifInterface import android.media.ExifInterface
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData import org.linphone.R
import org.linphone.contact.* import org.linphone.contact.*
import org.linphone.core.ChatRoomSecurityLevel import org.linphone.core.ChatRoomSecurityLevel
import org.linphone.core.Friend import org.linphone.core.Friend
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.ImageUtils import org.linphone.utils.ImageUtils
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
class ContactEditorViewModelFactory(private val friend: Friend?) : class ContactEditorData(val friend: Friend?) : ContactDataInterface {
ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ContactEditorViewModel(friend) as T
}
}
class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>() override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>() override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>() override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
override val coroutineScope: CoroutineScope = viewModelScope override val coroutineScope: CoroutineScope = coreContext.coroutineScope
val firstName = MutableLiveData<String>() val firstName = MutableLiveData<String>()
@ -74,20 +63,20 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
var syncAccountType: String? = null var syncAccountType: String? = null
init { init {
if (c != null) { if (friend != null) {
contact.value = c!! contact.value = friend!!
displayName.value = c.name ?: "" displayName.value = friend.name ?: ""
} else { } else {
displayName.value = "" displayName.value = ""
} }
organization.value = c?.organization ?: "" organization.value = friend?.organization ?: ""
firstName.value = "" firstName.value = ""
lastName.value = "" lastName.value = ""
val vCard = c?.vcard val refKey = friend?.refKey
val vCard = friend?.vcard
if (vCard?.familyName.isNullOrEmpty() && vCard?.givenName.isNullOrEmpty()) { if (vCard?.familyName.isNullOrEmpty() && vCard?.givenName.isNullOrEmpty()) {
val refKey = c?.refKey
if (refKey != null) { if (refKey != null) {
Log.w("[Contact Editor] vCard first & last name not filled-in yet, doing it now") Log.w("[Contact Editor] vCard first & last name not filled-in yet, doing it now")
fetchFirstAndLastNames(refKey) fetchFirstAndLastNames(refKey)
@ -99,11 +88,11 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
lastName.value = vCard?.familyName lastName.value = vCard?.familyName
} }
updateNumbersAndAddresses() updateNumbersAndAddresses(refKey)
} }
fun save(): Friend { fun save(): Friend {
var contact = c var contact = friend
var created = false var created = false
if (contact == null) { if (contact == null) {
@ -144,7 +133,7 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
} }
for (address in addresses.value.orEmpty()) { for (address in addresses.value.orEmpty()) {
val sipAddress = address.newValue.value.orEmpty() val sipAddress = address.newValue.value.orEmpty()
if (sipAddress.isEmpty()) continue if (sipAddress.isEmpty() || address.toRemove.value == true) continue
val parsed = coreContext.core.interpretUrl(sipAddress, false) val parsed = coreContext.core.interpretUrl(sipAddress, false)
if (parsed != null) contact.addAddress(parsed) if (parsed != null) contact.addAddress(parsed)
@ -155,7 +144,7 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
} }
for (phone in numbers.value.orEmpty()) { for (phone in numbers.value.orEmpty()) {
val phoneNumber = phone.newValue.value.orEmpty() val phoneNumber = phone.newValue.value.orEmpty()
if (phoneNumber.isEmpty()) continue if (phoneNumber.isEmpty() || phone.toRemove.value == true) continue
contact.addPhoneNumber(phoneNumber) contact.addPhoneNumber(phoneNumber)
} }
@ -228,20 +217,71 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
numbers.value = list numbers.value = list
} }
private fun updateNumbersAndAddresses() { private fun updateNumbersAndAddresses(contactId: String?) {
val phoneNumbers = arrayListOf<NumberOrAddressEditorData>() val phoneNumbers = arrayListOf<NumberOrAddressEditorData>()
for (number in c?.phoneNumbers.orEmpty()) { val sipAddresses = arrayListOf<NumberOrAddressEditorData>()
phoneNumbers.add(NumberOrAddressEditorData(number, false)) var fetched = false
if (contactId != null) {
try {
// Try to get real values from contact to ensure edition/removal in native address book will go well
val cursor = coreContext.context.contentResolver.query(
ContactsContract.Data.CONTENT_URI,
arrayOf(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER
),
ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?",
arrayOf(contactId),
null
)
while (cursor != null && cursor.moveToNext()) {
val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type)
val mime: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE))
if (mime == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) {
val data1: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER))
if (data1 != null) {
phoneNumbers.add(NumberOrAddressEditorData(data1, false))
}
} else if (
mime == ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE ||
mime == linphoneMime
) {
val data1: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS))
if (data1 != null) {
sipAddresses.add(NumberOrAddressEditorData(data1, true))
}
}
}
cursor?.close()
fetched = true
} catch (e: Exception) {
Log.e("[Contact Editor] Failed to sip addresses & phone number: $e")
fetched = false
}
} }
if (!fetched) {
Log.w("[Contact Editor] Fall-backing to friend info (might be inaccurate and thus edition/removal might fail)")
for (number in friend?.phoneNumbers.orEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData(number, false))
}
for (address in friend?.addresses.orEmpty()) {
sipAddresses.add(NumberOrAddressEditorData(address.asStringUriOnly(), true))
}
}
if (phoneNumbers.isEmpty()) { if (phoneNumbers.isEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData("", false)) phoneNumbers.add(NumberOrAddressEditorData("", false))
} }
numbers.value = phoneNumbers numbers.value = phoneNumbers
val sipAddresses = arrayListOf<NumberOrAddressEditorData>()
for (address in c?.addresses.orEmpty()) {
sipAddresses.add(NumberOrAddressEditorData(address.asStringUriOnly(), true))
}
if (sipAddresses.isEmpty()) { if (sipAddresses.isEmpty()) {
sipAddresses.add(NumberOrAddressEditorData("", true)) sipAddresses.add(NumberOrAddressEditorData("", true))
} }
@ -268,14 +308,14 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
val givenName: String? = val givenName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME))
if (!givenName.isNullOrEmpty()) { if (!givenName.isNullOrEmpty()) {
c?.vcard?.givenName = givenName friend?.vcard?.givenName = givenName
firstName.value = givenName!! firstName.value = givenName!!
} }
val familyName: String? = val familyName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME))
if (!familyName.isNullOrEmpty()) { if (!familyName.isNullOrEmpty()) {
c?.vcard?.familyName = familyName friend?.vcard?.familyName = familyName
lastName.value = familyName!! lastName.value = familyName!!
} }
} }

View file

@ -27,7 +27,6 @@ import android.os.Parcelable
import android.provider.MediaStore import android.provider.MediaStore
import android.view.View import android.view.View
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import java.io.File import java.io.File
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -35,6 +34,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.GenericFragment import org.linphone.activities.GenericFragment
import org.linphone.activities.main.MainActivity import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.contact.data.ContactEditorData
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
import org.linphone.activities.main.contact.viewmodels.* import org.linphone.activities.main.contact.viewmodels.*
import org.linphone.activities.navigateToContact import org.linphone.activities.navigateToContact
@ -44,7 +44,7 @@ import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), SyncAccountPickerFragment.SyncAccountPickedListener { class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), SyncAccountPickerFragment.SyncAccountPickedListener {
private lateinit var viewModel: ContactEditorViewModel private lateinit var data: ContactEditorData
private var temporaryPicturePath: File? = null private var temporaryPicturePath: File? = null
override fun getLayoutId(): Int = R.layout.contact_editor_fragment override fun getLayoutId(): Int = R.layout.contact_editor_fragment
@ -54,11 +54,10 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider( val contact = sharedViewModel.selectedContact.value
this, // TODO: FIXME: contact can be const! Find a way to get it not-const!
ContactEditorViewModelFactory(sharedViewModel.selectedContact.value) data = ContactEditorData(contact)
)[ContactEditorViewModel::class.java] binding.viewModel = data
binding.viewModel = viewModel
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
@ -67,10 +66,10 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
} }
binding.setSaveChangesClickListener { binding.setSaveChangesClickListener {
viewModel.syncAccountName = null data.syncAccountName = null
viewModel.syncAccountType = null data.syncAccountType = null
if (viewModel.c == null && corePreferences.showNewContactAccountDialog) { if (data.friend == null && corePreferences.showNewContactAccountDialog) {
Log.i("[Contact Editor] New contact, ask user where to store it") Log.i("[Contact Editor] New contact, ask user where to store it")
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
} else { } else {
@ -85,9 +84,9 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
newSipUri.newValue.value = sipUri newSipUri.newValue.value = sipUri
val list = arrayListOf<NumberOrAddressEditorData>() val list = arrayListOf<NumberOrAddressEditorData>()
list.addAll(viewModel.addresses.value.orEmpty()) list.addAll(data.addresses.value.orEmpty())
list.add(newSipUri) list.add(newSipUri)
viewModel.addresses.value = list data.addresses.value = list
} }
if (!PermissionHelper.required(requireContext()).hasWriteContactsPermission()) { if (!PermissionHelper.required(requireContext()).hasWriteContactsPermission()) {
@ -98,8 +97,8 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
override fun onSyncAccountClicked(name: String?, type: String?) { override fun onSyncAccountClicked(name: String?, type: String?) {
Log.i("[Contact Editor] Using account $name / $type") Log.i("[Contact Editor] Using account $name / $type")
viewModel.syncAccountName = name data.syncAccountName = name
viewModel.syncAccountType = type data.syncAccountType = type
saveContact() saveContact()
} }
@ -122,19 +121,19 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
val contactImageFilePath = FileUtils.getFilePathFromPickerIntent(data, temporaryPicturePath) val contactImageFilePath = FileUtils.getFilePathFromPickerIntent(intent, temporaryPicturePath)
if (contactImageFilePath != null) { if (contactImageFilePath != null) {
viewModel.setPictureFromPath(contactImageFilePath) data.setPictureFromPath(contactImageFilePath)
} }
} }
} }
} }
private fun saveContact() { private fun saveContact() {
val savedContact = viewModel.save() val savedContact = data.save()
val id = savedContact.refKey val id = savedContact.refKey
if (id != null) { if (id != null) {
Log.i("[Contact Editor] Displaying contact $savedContact") Log.i("[Contact Editor] Displaying contact $savedContact")

View file

@ -12,7 +12,7 @@
type="android.view.View.OnClickListener"/> type="android.view.View.OnClickListener"/>
<variable <variable
name="viewModel" name="viewModel"
type="org.linphone.activities.main.contact.viewmodels.ContactEditorViewModel" /> type="org.linphone.activities.main.contact.data.ContactEditorData" />
</data> </data>
<RelativeLayout <RelativeLayout