mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Improved startup reactivity
This commit is contained in:
parent
f6545f5641
commit
26e30c6060
4 changed files with 422 additions and 406 deletions
|
|
@ -25,6 +25,7 @@ import android.os.Bundle
|
|||
import android.provider.ContactsContract
|
||||
import android.util.Patterns
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.CursorLoader
|
||||
import androidx.loader.content.Loader
|
||||
|
|
@ -56,6 +57,8 @@ class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
|
|||
private const val MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH = 300000L // 5 minutes
|
||||
}
|
||||
|
||||
val friends = HashMap<String, Friend>()
|
||||
|
||||
@MainThread
|
||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
|
||||
val mimeType = ContactsContract.Data.MIMETYPE
|
||||
|
|
@ -92,322 +95,8 @@ class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
|
|||
}
|
||||
Log.i("$TAG Load finished, found ${cursor.count} entries in cursor")
|
||||
|
||||
coreContext.postOnCoreThread { core ->
|
||||
val state = coreContext.core.globalState
|
||||
if (state == GlobalState.Shutdown || state == GlobalState.Off) {
|
||||
Log.w("$TAG Core is being stopped or already destroyed, abort")
|
||||
return@postOnCoreThread
|
||||
}
|
||||
|
||||
val friends = HashMap<String, Friend>()
|
||||
try {
|
||||
// Cursor can be null now that we are on a different dispatcher according to Crashlytics
|
||||
val friendsPhoneNumbers = arrayListOf<String>()
|
||||
val friendsAddresses = arrayListOf<Address>()
|
||||
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
|
||||
|
||||
val uri = friend.getNativeContactPictureUri()
|
||||
if (uri != null) {
|
||||
friend.photo = uri.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"
|
||||
|
||||
friend.isSubscribesEnabled = false
|
||||
// Disable peer to peer short term presence
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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, false)
|
||||
if (address != null &&
|
||||
friendsAddresses.find {
|
||||
it.weakEqual(address)
|
||||
} == null
|
||||
) {
|
||||
friend.addAddress(address)
|
||||
friendsAddresses.add(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
|
||||
val organization: String? =
|
||||
cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Organization.COMPANY
|
||||
)
|
||||
)
|
||||
if (organization != null) {
|
||||
friend.organization = organization
|
||||
}
|
||||
|
||||
val job: String? =
|
||||
cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Organization.TITLE
|
||||
)
|
||||
)
|
||||
if (job != null) {
|
||||
friend.jobTitle = job
|
||||
}
|
||||
}
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
val vCard = friend.vcard
|
||||
if (vCard != null) {
|
||||
val givenName: String? =
|
||||
cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME
|
||||
)
|
||||
)
|
||||
if (!givenName.isNullOrEmpty()) {
|
||||
vCard.givenName = givenName
|
||||
}
|
||||
|
||||
val familyName: String? =
|
||||
cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME
|
||||
)
|
||||
)
|
||||
if (!familyName.isNullOrEmpty()) {
|
||||
vCard.familyName = familyName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
friends[id] = friend
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Exception: $e")
|
||||
}
|
||||
}
|
||||
|
||||
if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) {
|
||||
Log.w("$TAG Core is being stopped or already destroyed, abort")
|
||||
} else if (friends.isEmpty()) {
|
||||
Log.w("$TAG No friend created!")
|
||||
} else {
|
||||
Log.i("$TAG ${friends.size} friends fetched")
|
||||
|
||||
val friendsList = core.getFriendListByName(NATIVE_ADDRESS_BOOK_FRIEND_LIST) ?: core.createFriendList()
|
||||
if (friendsList.displayName.isNullOrEmpty()) {
|
||||
Log.i(
|
||||
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] didn't exist yet, let's create it"
|
||||
)
|
||||
friendsList.isDatabaseStorageEnabled = true // Store them to keep presence info available for push notifications & favorites
|
||||
friendsList.type = FriendList.Type.Default
|
||||
friendsList.displayName = NATIVE_ADDRESS_BOOK_FRIEND_LIST
|
||||
core.addFriendList(friendsList)
|
||||
|
||||
for (friend in friends.values) {
|
||||
friendsList.addLocalFriend(friend)
|
||||
}
|
||||
Log.i("$TAG Friends added")
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] found, synchronizing existing friends with new ones"
|
||||
)
|
||||
for (localFriend in friendsList.friends) {
|
||||
val newlyFetchedFriend = friends[localFriend.refKey]
|
||||
if (newlyFetchedFriend != null) {
|
||||
Log.d(
|
||||
"$TAG Friend [${localFriend.name}] with ref key [${localFriend.refKey}] found in newly fetched batch"
|
||||
)
|
||||
localFriend.edit()
|
||||
localFriend.nativeUri = newlyFetchedFriend.nativeUri // Native URI isn't stored in linphone database, needs to be updated
|
||||
|
||||
// Update basic fields that may have changed
|
||||
localFriend.name = newlyFetchedFriend.name
|
||||
localFriend.organization = newlyFetchedFriend.organization
|
||||
localFriend.jobTitle = newlyFetchedFriend.jobTitle
|
||||
localFriend.photo = newlyFetchedFriend.photo
|
||||
|
||||
// Clear local friend phone numbers & add all newly fetched one ones
|
||||
var atLeastAPhoneNumberWasRemoved = false
|
||||
for (phoneNumber in localFriend.phoneNumbersWithLabel) {
|
||||
val found = newlyFetchedFriend.phoneNumbers.find {
|
||||
it == phoneNumber.phoneNumber
|
||||
}
|
||||
if (found == null) {
|
||||
atLeastAPhoneNumberWasRemoved = true
|
||||
}
|
||||
localFriend.removePhoneNumberWithLabel(phoneNumber)
|
||||
}
|
||||
for (phoneNumber in newlyFetchedFriend.phoneNumbersWithLabel) {
|
||||
localFriend.addPhoneNumberWithLabel(phoneNumber)
|
||||
}
|
||||
|
||||
// If at least a phone number was removed, remove all SIP address from local friend before adding all from newly fetched one.
|
||||
// If none was removed, simply add SIP addresses from fetched contact that aren't already in the local friend.
|
||||
if (atLeastAPhoneNumberWasRemoved) {
|
||||
Log.w(
|
||||
"$TAG At least a phone number was removed from native contact [${localFriend.name}], clearing all SIP addresses from local friend before adding back the ones that still exists"
|
||||
)
|
||||
for (sipAddress in localFriend.addresses) {
|
||||
localFriend.removeAddress(sipAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// Adding only newly added SIP address(es) in native contact if any
|
||||
for (sipAddress in newlyFetchedFriend.addresses) {
|
||||
val found = localFriend.addresses.find {
|
||||
it.weakEqual(sipAddress)
|
||||
}
|
||||
if (found == null) {
|
||||
localFriend.addAddress(sipAddress)
|
||||
}
|
||||
}
|
||||
localFriend.done()
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Friend [${localFriend.name}] with ref key [${localFriend.refKey}] not found in newly fetched batch, removing it"
|
||||
)
|
||||
friendsList.removeFriend(localFriend)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for newly created friends since last sync
|
||||
val localFriends = friendsList.friends
|
||||
for (key in friends.keys) {
|
||||
val found = localFriends.find {
|
||||
it.refKey == key
|
||||
}
|
||||
if (found == null) {
|
||||
val newFriend = friends[key]
|
||||
if (newFriend != null) {
|
||||
Log.i(
|
||||
"$TAG Friend [${newFriend.name}] with ref key [${newFriend.refKey}] not found in currently stored list, adding it"
|
||||
)
|
||||
friendsList.addLocalFriend(newFriend)
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Expected to find newly fetched friend with ref key [$key] but was null!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("$TAG Friends synchronized")
|
||||
}
|
||||
friends.clear()
|
||||
|
||||
friendsList.updateSubscriptions()
|
||||
Log.i("$TAG Subscription(s) updated")
|
||||
coreContext.contactsManager.onNativeContactsLoaded()
|
||||
}
|
||||
} catch (sde: StaleDataException) {
|
||||
Log.e("$TAG State Data Exception: $sde")
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("$TAG Illegal State Exception: $ise")
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Exception: $e")
|
||||
}
|
||||
coreContext.postOnCoreThread {
|
||||
parseFriends(cursor)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -415,4 +104,311 @@ class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
|
|||
override fun onLoaderReset(loader: Loader<Cursor>) {
|
||||
Log.i("$TAG Loader reset")
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun parseFriends(cursor: Cursor) {
|
||||
val core = coreContext.core
|
||||
|
||||
val state = core.globalState
|
||||
if (state == GlobalState.Shutdown || state == GlobalState.Off) {
|
||||
Log.w("$TAG Core is being stopped or already destroyed, abort")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val friendsPhoneNumbers = arrayListOf<String>()
|
||||
val friendsAddresses = arrayListOf<Address>()
|
||||
var previousId = ""
|
||||
|
||||
val contactIdColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID)
|
||||
val mimetypeColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE)
|
||||
val displayNameColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.Data.DISPLAY_NAME_PRIMARY
|
||||
)
|
||||
val starredColumn = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)
|
||||
val lookupColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.Contacts.LOOKUP_KEY
|
||||
)
|
||||
val phoneNumberColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER
|
||||
)
|
||||
val phoneTypeColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Phone.TYPE
|
||||
)
|
||||
val phoneLabelColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Phone.LABEL
|
||||
)
|
||||
val normalizedPhoneColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
|
||||
)
|
||||
val sipAddressColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS
|
||||
)
|
||||
val companyColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Organization.COMPANY
|
||||
)
|
||||
val jobTitleColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.Organization.TITLE
|
||||
)
|
||||
val givenNameColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME
|
||||
)
|
||||
val familyNameColumn = cursor.getColumnIndexOrThrow(
|
||||
ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME
|
||||
)
|
||||
while (!cursor.isClosed && cursor.moveToNext()) {
|
||||
try {
|
||||
val id: String = cursor.getString(contactIdColumn)
|
||||
val mime: String? = cursor.getString(mimetypeColumn)
|
||||
|
||||
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(displayNameColumn)
|
||||
friend.name = displayName
|
||||
|
||||
val uri = friend.getNativeContactPictureUri()
|
||||
if (uri != null) {
|
||||
friend.photo = uri.toString()
|
||||
}
|
||||
|
||||
val starred = cursor.getInt(starredColumn) == 1
|
||||
friend.starred = starred
|
||||
|
||||
val lookupKey =
|
||||
cursor.getString(lookupColumn)
|
||||
friend.nativeUri =
|
||||
"${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey"
|
||||
|
||||
friend.isSubscribesEnabled = false
|
||||
// Disable peer to peer short term presence
|
||||
friend.incSubscribePolicy = SubscribePolicy.SPDeny
|
||||
}
|
||||
|
||||
when (mime) {
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
|
||||
val data1: String? = cursor.getString(phoneNumberColumn)
|
||||
val data2: String? = cursor.getString(phoneTypeColumn)
|
||||
val data3: String? = cursor.getString(phoneLabelColumn)
|
||||
val data4: String? = cursor.getString(normalizedPhoneColumn)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
|
||||
val sipAddress: String? = cursor.getString(sipAddressColumn)
|
||||
if (sipAddress != null) {
|
||||
val address = core.interpretUrl(sipAddress, false)
|
||||
if (address != null &&
|
||||
friendsAddresses.find {
|
||||
it.weakEqual(address)
|
||||
} == null
|
||||
) {
|
||||
friend.addAddress(address)
|
||||
friendsAddresses.add(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
|
||||
val organization: String? = cursor.getString(companyColumn)
|
||||
if (organization != null) {
|
||||
friend.organization = organization
|
||||
}
|
||||
|
||||
val job: String? = cursor.getString(jobTitleColumn)
|
||||
if (job != null) {
|
||||
friend.jobTitle = job
|
||||
}
|
||||
}
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
|
||||
val vCard = friend.vcard
|
||||
if (vCard != null) {
|
||||
val givenName: String? = cursor.getString(givenNameColumn)
|
||||
if (!givenName.isNullOrEmpty()) {
|
||||
vCard.givenName = givenName
|
||||
}
|
||||
|
||||
val familyName: String? = cursor.getString(familyNameColumn)
|
||||
if (!familyName.isNullOrEmpty()) {
|
||||
vCard.familyName = familyName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
friends[id] = friend
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Exception: $e")
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("$TAG Contacts parsed, posting another task to handle adding them (or not)")
|
||||
// Re-post another task to allow other tasks on Core thread
|
||||
coreContext.postOnCoreThread {
|
||||
addFriendsIfNeeded()
|
||||
}
|
||||
} catch (sde: StaleDataException) {
|
||||
Log.e("$TAG State Data Exception: $sde")
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("$TAG Illegal State Exception: $ise")
|
||||
} catch (e: Exception) {
|
||||
Log.e("$TAG Exception: $e")
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun addFriendsIfNeeded() {
|
||||
val core = coreContext.core
|
||||
|
||||
if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) {
|
||||
Log.w("$TAG Core is being stopped or already destroyed, abort")
|
||||
} else if (friends.isEmpty()) {
|
||||
Log.w("$TAG No friend created!")
|
||||
} else {
|
||||
Log.i("$TAG ${friends.size} friends fetched")
|
||||
|
||||
val friendsList = core.getFriendListByName(NATIVE_ADDRESS_BOOK_FRIEND_LIST)
|
||||
?: core.createFriendList()
|
||||
if (friendsList.displayName.isNullOrEmpty()) {
|
||||
Log.i(
|
||||
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] didn't exist yet, let's create it"
|
||||
)
|
||||
friendsList.isDatabaseStorageEnabled =
|
||||
true // Store them to keep presence info available for push notifications & favorites
|
||||
friendsList.type = FriendList.Type.Default
|
||||
friendsList.displayName = NATIVE_ADDRESS_BOOK_FRIEND_LIST
|
||||
core.addFriendList(friendsList)
|
||||
|
||||
for (friend in friends.values) {
|
||||
friendsList.addLocalFriend(friend)
|
||||
}
|
||||
Log.i("$TAG Friends added")
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Friend list [$NATIVE_ADDRESS_BOOK_FRIEND_LIST] found, synchronizing existing friends with new ones"
|
||||
)
|
||||
for (localFriend in friendsList.friends) {
|
||||
val newlyFetchedFriend = friends[localFriend.refKey]
|
||||
if (newlyFetchedFriend != null) {
|
||||
Log.d(
|
||||
"$TAG Friend [${localFriend.name}] with ref key [${localFriend.refKey}] found in newly fetched batch"
|
||||
)
|
||||
localFriend.edit()
|
||||
localFriend.nativeUri =
|
||||
newlyFetchedFriend.nativeUri // Native URI isn't stored in linphone database, needs to be updated
|
||||
|
||||
// Update basic fields that may have changed
|
||||
localFriend.name = newlyFetchedFriend.name
|
||||
localFriend.organization = newlyFetchedFriend.organization
|
||||
localFriend.jobTitle = newlyFetchedFriend.jobTitle
|
||||
localFriend.photo = newlyFetchedFriend.photo
|
||||
|
||||
// Clear local friend phone numbers & add all newly fetched one ones
|
||||
var atLeastAPhoneNumberWasRemoved = false
|
||||
for (phoneNumber in localFriend.phoneNumbersWithLabel) {
|
||||
val found = newlyFetchedFriend.phoneNumbers.find {
|
||||
it == phoneNumber.phoneNumber
|
||||
}
|
||||
if (found == null) {
|
||||
atLeastAPhoneNumberWasRemoved = true
|
||||
}
|
||||
localFriend.removePhoneNumberWithLabel(phoneNumber)
|
||||
}
|
||||
for (phoneNumber in newlyFetchedFriend.phoneNumbersWithLabel) {
|
||||
localFriend.addPhoneNumberWithLabel(phoneNumber)
|
||||
}
|
||||
|
||||
// If at least a phone number was removed, remove all SIP address from local friend before adding all from newly fetched one.
|
||||
// If none was removed, simply add SIP addresses from fetched contact that aren't already in the local friend.
|
||||
if (atLeastAPhoneNumberWasRemoved) {
|
||||
Log.w(
|
||||
"$TAG At least a phone number was removed from native contact [${localFriend.name}], clearing all SIP addresses from local friend before adding back the ones that still exists"
|
||||
)
|
||||
for (sipAddress in localFriend.addresses) {
|
||||
localFriend.removeAddress(sipAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// Adding only newly added SIP address(es) in native contact if any
|
||||
for (sipAddress in newlyFetchedFriend.addresses) {
|
||||
val found = localFriend.addresses.find {
|
||||
it.weakEqual(sipAddress)
|
||||
}
|
||||
if (found == null) {
|
||||
localFriend.addAddress(sipAddress)
|
||||
}
|
||||
}
|
||||
localFriend.done()
|
||||
} else {
|
||||
Log.i(
|
||||
"$TAG Friend [${localFriend.name}] with ref key [${localFriend.refKey}] not found in newly fetched batch, removing it"
|
||||
)
|
||||
friendsList.removeFriend(localFriend)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for newly created friends since last sync
|
||||
val localFriends = friendsList.friends
|
||||
for (key in friends.keys) {
|
||||
val found = localFriends.find {
|
||||
it.refKey == key
|
||||
}
|
||||
if (found == null) {
|
||||
val newFriend = friends[key]
|
||||
if (newFriend != null) {
|
||||
Log.i(
|
||||
"$TAG Friend [${newFriend.name}] with ref key [${newFriend.refKey}] not found in currently stored list, adding it"
|
||||
)
|
||||
friendsList.addLocalFriend(newFriend)
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Expected to find newly fetched friend with ref key [$key] but was null!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i("$TAG Friends synchronized")
|
||||
}
|
||||
friends.clear()
|
||||
|
||||
friendsList.updateSubscriptions()
|
||||
Log.i("$TAG Subscription(s) updated")
|
||||
coreContext.contactsManager.onNativeContactsLoaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class ContactsManager @UiThread constructor() {
|
|||
|
||||
@MainThread
|
||||
fun loadContacts(activity: MainActivity) {
|
||||
Log.i("$TAG Starting contacts loader")
|
||||
val manager = LoaderManager.getInstance(activity)
|
||||
manager.restartLoader(0, null, ContactLoader())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import androidx.annotation.MainThread
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.findNavController
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
|
@ -82,6 +82,20 @@ class MainActivity : GenericActivity() {
|
|||
|
||||
private var currentlyDisplayedAuthDialog: Dialog? = null
|
||||
|
||||
private var navigatedToDefaultFragment = false
|
||||
|
||||
private val destinationListener = object : NavController.OnDestinationChangedListener {
|
||||
override fun onDestinationChanged(
|
||||
controller: NavController,
|
||||
destination: NavDestination,
|
||||
arguments: Bundle?
|
||||
) {
|
||||
Log.i("$TAG Latest visited fragment was restored")
|
||||
navigatedToDefaultFragment = true
|
||||
controller.removeOnDestinationChangedListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Must be done before the setContentView
|
||||
installSplashScreen()
|
||||
|
|
@ -165,14 +179,23 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
binding.root.doOnAttach {
|
||||
Log.i("$TAG Report UI has been fully drawn (TTFD)")
|
||||
try {
|
||||
reportFullyDrawn()
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("$TAG Security exception when doing reportFullyDrawn(): $se")
|
||||
// Wait for latest visited fragment to be displayed before hiding the splashscreen
|
||||
binding.root.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
return if (navigatedToDefaultFragment) {
|
||||
Log.i("$TAG Report UI has been fully drawn (TTFD)")
|
||||
try {
|
||||
reportFullyDrawn()
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("$TAG Security exception when doing reportFullyDrawn(): $se")
|
||||
}
|
||||
binding.root.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
coreContext.bearerAuthenticationRequestedEvent.observe(this) {
|
||||
it.consume { pair ->
|
||||
|
|
@ -224,8 +247,10 @@ class MainActivity : GenericActivity() {
|
|||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
goToLatestVisitedFragment()
|
||||
|
||||
if (intent != null) {
|
||||
handleIntent(intent, false)
|
||||
handleIntent(intent)
|
||||
} else {
|
||||
// This should never happen!
|
||||
Log.e("$TAG onPostCreate called without intent !")
|
||||
|
|
@ -270,7 +295,7 @@ class MainActivity : GenericActivity() {
|
|||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent, true)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
|
|
@ -294,8 +319,71 @@ class MainActivity : GenericActivity() {
|
|||
return findNavController(R.id.main_nav_host_fragment)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleIntent(intent: Intent, isNewIntent: Boolean) {
|
||||
private fun goToLatestVisitedFragment() {
|
||||
try {
|
||||
// Prevent navigating to default fragment upon rotation (we only want to do it on first start)
|
||||
if (intent.action == Intent.ACTION_MAIN && intent.type == null && intent.data == null) {
|
||||
if (viewModel.mainIntentHandled) {
|
||||
Log.d(
|
||||
"$TAG Main intent without type nor data was already handled, do nothing"
|
||||
)
|
||||
} else {
|
||||
viewModel.mainIntentHandled = true
|
||||
}
|
||||
}
|
||||
|
||||
val defaultFragmentId = getPreferences(Context.MODE_PRIVATE).getInt(
|
||||
DEFAULT_FRAGMENT_KEY,
|
||||
HISTORY_FRAGMENT_ID
|
||||
)
|
||||
Log.i(
|
||||
"$TAG Trying to navigate to set default destination [$defaultFragmentId]"
|
||||
)
|
||||
val args = intent.extras
|
||||
try {
|
||||
val navOptionsBuilder = NavOptions.Builder()
|
||||
navOptionsBuilder.setPopUpTo(R.id.historyListFragment, true)
|
||||
navOptionsBuilder.setLaunchSingleTop(true)
|
||||
val navOptions = navOptionsBuilder.build()
|
||||
when (defaultFragmentId) {
|
||||
CONTACTS_FRAGMENT_ID -> {
|
||||
findNavController().addOnDestinationChangedListener(destinationListener)
|
||||
findNavController().navigate(
|
||||
R.id.contactsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
CHAT_FRAGMENT_ID -> {
|
||||
findNavController().addOnDestinationChangedListener(destinationListener)
|
||||
findNavController().navigate(
|
||||
R.id.conversationsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
MEETINGS_FRAGMENT_ID -> {
|
||||
findNavController().addOnDestinationChangedListener(destinationListener)
|
||||
findNavController().navigate(
|
||||
R.id.meetingsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Log.i("$TAG Default fragment is the same as the latest visited one")
|
||||
navigatedToDefaultFragment = true
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("$TAG Can't navigate to Conversations fragment: $ise")
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.i("$TAG Failed to handle intent: $ise")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
Log.i(
|
||||
"$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]"
|
||||
)
|
||||
|
|
@ -326,12 +414,11 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
else -> {
|
||||
handleMainIntent(intent, isNewIntent)
|
||||
handleMainIntent(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleLocusOrShortcut(id: String) {
|
||||
Log.i("$TAG Found locus ID [$id]")
|
||||
val pair = LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(id)
|
||||
|
|
@ -345,8 +432,7 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleMainIntent(intent: Intent, isNewIntent: Boolean) {
|
||||
private fun handleMainIntent(intent: Intent) {
|
||||
coreContext.postOnCoreThread { core ->
|
||||
if (corePreferences.firstLaunch) {
|
||||
Log.i("$TAG First time Linphone 6.0 has been started, showing Welcome activity")
|
||||
|
|
@ -361,18 +447,6 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
} else {
|
||||
coreContext.postOnMainThread {
|
||||
// Prevent navigating to default fragment upon rotation (we only want to do it on first start)
|
||||
if (intent.action == Intent.ACTION_MAIN && intent.type == null && intent.data == null && !isNewIntent) {
|
||||
if (viewModel.mainIntentHandled) {
|
||||
Log.d(
|
||||
"$TAG Main intent without type nor data was already handled, do nothing"
|
||||
)
|
||||
return@postOnMainThread
|
||||
} else {
|
||||
viewModel.mainIntentHandled = true
|
||||
}
|
||||
}
|
||||
|
||||
if (intent.hasExtra("Chat")) {
|
||||
Log.i("$TAG Intent has [Chat] extra")
|
||||
try {
|
||||
|
|
@ -404,64 +478,12 @@ class MainActivity : GenericActivity() {
|
|||
} catch (ise: IllegalStateException) {
|
||||
Log.e("$TAG Can't navigate to Conversations fragment: $ise")
|
||||
}
|
||||
} else if (!isNewIntent) {
|
||||
try {
|
||||
val defaultFragmentId = getPreferences(Context.MODE_PRIVATE).getInt(
|
||||
DEFAULT_FRAGMENT_KEY,
|
||||
HISTORY_FRAGMENT_ID
|
||||
)
|
||||
Log.i(
|
||||
"$TAG Trying to navigate to set default destination [$defaultFragmentId]"
|
||||
)
|
||||
val args = intent.extras
|
||||
try {
|
||||
val navOptionsBuilder = NavOptions.Builder()
|
||||
navOptionsBuilder.setPopUpTo(R.id.historyListFragment, true)
|
||||
navOptionsBuilder.setLaunchSingleTop(true)
|
||||
val navOptions = navOptionsBuilder.build()
|
||||
when (defaultFragmentId) {
|
||||
HISTORY_FRAGMENT_ID -> {
|
||||
findNavController().navigate(
|
||||
R.id.historyListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
CONTACTS_FRAGMENT_ID -> {
|
||||
findNavController().navigate(
|
||||
R.id.contactsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
CHAT_FRAGMENT_ID -> {
|
||||
findNavController().navigate(
|
||||
R.id.conversationsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
MEETINGS_FRAGMENT_ID -> {
|
||||
findNavController().navigate(
|
||||
R.id.meetingsListFragment,
|
||||
args,
|
||||
navOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e("$TAG Can't navigate to Conversations fragment: $ise")
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.i("$TAG Failed to handle intent: $ise")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleSendIntent(intent: Intent, multiple: Boolean) {
|
||||
val parcelablesUri = arrayListOf<Uri>()
|
||||
|
||||
|
|
@ -550,7 +572,6 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun parseShortcutIfAny(intent: Intent): Pair<String, String>? {
|
||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||
if (shortcutId != null) {
|
||||
|
|
@ -562,7 +583,6 @@ class MainActivity : GenericActivity() {
|
|||
return null
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleCallIntent(intent: Intent) {
|
||||
val uri = intent.data?.toString()
|
||||
if (uri.isNullOrEmpty()) {
|
||||
|
|
@ -595,7 +615,6 @@ class MainActivity : GenericActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun handleConfigIntent(uri: String) {
|
||||
val remoteConfigUri = uri.substring("linphone-config:".length)
|
||||
val url = when {
|
||||
|
|
|
|||
|
|
@ -699,6 +699,12 @@ class MessageModel @WorkerThread constructor(
|
|||
|
||||
@WorkerThread
|
||||
private fun startVoiceRecordPlayer() {
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("$TAG Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
|
|
@ -712,12 +718,6 @@ class MessageModel @WorkerThread constructor(
|
|||
)
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
Log.i("$TAG Playing voice record")
|
||||
isPlayingVoiceRecord.postValue(true)
|
||||
voiceRecordPlayer.start()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue