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
- 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
- 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
- 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

View file

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

View file

@ -17,43 +17,32 @@
* You should have received a copy of the GNU General Public License
* 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.BitmapFactory
import android.media.ExifInterface
import android.provider.ContactsContract
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import java.io.ByteArrayOutputStream
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import org.linphone.LinphoneApplication.Companion.coreContext
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.core.ChatRoomSecurityLevel
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.ImageUtils
import org.linphone.utils.PermissionHelper
class ContactEditorViewModelFactory(private val friend: Friend?) :
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 {
class ContactEditorData(val friend: Friend?) : ContactDataInterface {
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
override val coroutineScope: CoroutineScope = viewModelScope
override val coroutineScope: CoroutineScope = coreContext.coroutineScope
val firstName = MutableLiveData<String>()
@ -74,20 +63,20 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
var syncAccountType: String? = null
init {
if (c != null) {
contact.value = c!!
displayName.value = c.name ?: ""
if (friend != null) {
contact.value = friend!!
displayName.value = friend.name ?: ""
} else {
displayName.value = ""
}
organization.value = c?.organization ?: ""
organization.value = friend?.organization ?: ""
firstName.value = ""
lastName.value = ""
val vCard = c?.vcard
val refKey = friend?.refKey
val vCard = friend?.vcard
if (vCard?.familyName.isNullOrEmpty() && vCard?.givenName.isNullOrEmpty()) {
val refKey = c?.refKey
if (refKey != null) {
Log.w("[Contact Editor] vCard first & last name not filled-in yet, doing it now")
fetchFirstAndLastNames(refKey)
@ -99,11 +88,11 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
lastName.value = vCard?.familyName
}
updateNumbersAndAddresses()
updateNumbersAndAddresses(refKey)
}
fun save(): Friend {
var contact = c
var contact = friend
var created = false
if (contact == null) {
@ -144,7 +133,7 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
}
for (address in addresses.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)
if (parsed != null) contact.addAddress(parsed)
@ -155,7 +144,7 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
}
for (phone in numbers.value.orEmpty()) {
val phoneNumber = phone.newValue.value.orEmpty()
if (phoneNumber.isEmpty()) continue
if (phoneNumber.isEmpty() || phone.toRemove.value == true) continue
contact.addPhoneNumber(phoneNumber)
}
@ -228,20 +217,71 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
numbers.value = list
}
private fun updateNumbersAndAddresses() {
private fun updateNumbersAndAddresses(contactId: String?) {
val phoneNumbers = arrayListOf<NumberOrAddressEditorData>()
for (number in c?.phoneNumbers.orEmpty()) {
phoneNumbers.add(NumberOrAddressEditorData(number, false))
val sipAddresses = arrayListOf<NumberOrAddressEditorData>()
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()) {
phoneNumbers.add(NumberOrAddressEditorData("", false))
}
numbers.value = phoneNumbers
val sipAddresses = arrayListOf<NumberOrAddressEditorData>()
for (address in c?.addresses.orEmpty()) {
sipAddresses.add(NumberOrAddressEditorData(address.asStringUriOnly(), true))
}
if (sipAddresses.isEmpty()) {
sipAddresses.add(NumberOrAddressEditorData("", true))
}
@ -268,14 +308,14 @@ class ContactEditorViewModel(val c: Friend?) : ViewModel(), ContactDataInterface
val givenName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME))
if (!givenName.isNullOrEmpty()) {
c?.vcard?.givenName = givenName
friend?.vcard?.givenName = givenName
firstName.value = givenName!!
}
val familyName: String? =
cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME))
if (!familyName.isNullOrEmpty()) {
c?.vcard?.familyName = familyName
friend?.vcard?.familyName = familyName
lastName.value = familyName!!
}
}

View file

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

View file

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