Improved startup reactivity

This commit is contained in:
Sylvain Berfini 2024-05-16 21:47:45 +02:00
parent f6545f5641
commit 26e30c6060
4 changed files with 422 additions and 406 deletions

View file

@ -25,6 +25,7 @@ import android.os.Bundle
import android.provider.ContactsContract import android.provider.ContactsContract
import android.util.Patterns import android.util.Patterns
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader 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 private const val MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH = 300000L // 5 minutes
} }
val friends = HashMap<String, Friend>()
@MainThread @MainThread
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
val mimeType = ContactsContract.Data.MIMETYPE 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") Log.i("$TAG Load finished, found ${cursor.count} entries in cursor")
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread {
val state = coreContext.core.globalState parseFriends(cursor)
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")
}
} }
} }
@ -415,4 +104,311 @@ class ContactLoader : LoaderManager.LoaderCallbacks<Cursor> {
override fun onLoaderReset(loader: Loader<Cursor>) { override fun onLoaderReset(loader: Loader<Cursor>) {
Log.i("$TAG Loader reset") 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()
}
}
} }

View file

@ -145,6 +145,7 @@ class ContactsManager @UiThread constructor() {
@MainThread @MainThread
fun loadContacts(activity: MainActivity) { fun loadContacts(activity: MainActivity) {
Log.i("$TAG Starting contacts loader")
val manager = LoaderManager.getInstance(activity) val manager = LoaderManager.getInstance(activity)
manager.restartLoader(0, null, ContactLoader()) manager.restartLoader(0, null, ContactLoader())
} }

View file

@ -29,14 +29,14 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.Gravity import android.view.Gravity
import androidx.annotation.MainThread import android.view.ViewTreeObserver
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.doOnAttach
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.findNavController import androidx.navigation.findNavController
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
@ -82,6 +82,20 @@ class MainActivity : GenericActivity() {
private var currentlyDisplayedAuthDialog: Dialog? = null 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?) { override fun onCreate(savedInstanceState: Bundle?) {
// Must be done before the setContentView // Must be done before the setContentView
installSplashScreen() installSplashScreen()
@ -165,14 +179,23 @@ class MainActivity : GenericActivity() {
} }
} }
binding.root.doOnAttach { // Wait for latest visited fragment to be displayed before hiding the splashscreen
Log.i("$TAG Report UI has been fully drawn (TTFD)") binding.root.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
try { override fun onPreDraw(): Boolean {
reportFullyDrawn() return if (navigatedToDefaultFragment) {
} catch (se: SecurityException) { Log.i("$TAG Report UI has been fully drawn (TTFD)")
Log.e("$TAG Security exception when doing reportFullyDrawn(): $se") 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) { coreContext.bearerAuthenticationRequestedEvent.observe(this) {
it.consume { pair -> it.consume { pair ->
@ -224,8 +247,10 @@ class MainActivity : GenericActivity() {
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
goToLatestVisitedFragment()
if (intent != null) { if (intent != null) {
handleIntent(intent, false) handleIntent(intent)
} else { } else {
// This should never happen! // This should never happen!
Log.e("$TAG onPostCreate called without intent !") Log.e("$TAG onPostCreate called without intent !")
@ -270,7 +295,7 @@ class MainActivity : GenericActivity() {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleIntent(intent, true) handleIntent(intent)
} }
@SuppressLint("RtlHardcoded") @SuppressLint("RtlHardcoded")
@ -294,8 +319,71 @@ class MainActivity : GenericActivity() {
return findNavController(R.id.main_nav_host_fragment) return findNavController(R.id.main_nav_host_fragment)
} }
@MainThread private fun goToLatestVisitedFragment() {
private fun handleIntent(intent: Intent, isNewIntent: Boolean) { 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( Log.i(
"$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]" "$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]"
) )
@ -326,12 +414,11 @@ class MainActivity : GenericActivity() {
} }
} }
else -> { else -> {
handleMainIntent(intent, isNewIntent) handleMainIntent(intent)
} }
} }
} }
@MainThread
private fun handleLocusOrShortcut(id: String) { private fun handleLocusOrShortcut(id: String) {
Log.i("$TAG Found locus ID [$id]") Log.i("$TAG Found locus ID [$id]")
val pair = LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(id) val pair = LinphoneUtils.getLocalAndPeerSipUrisFromChatRoomId(id)
@ -345,8 +432,7 @@ class MainActivity : GenericActivity() {
} }
} }
@MainThread private fun handleMainIntent(intent: Intent) {
private fun handleMainIntent(intent: Intent, isNewIntent: Boolean) {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
if (corePreferences.firstLaunch) { if (corePreferences.firstLaunch) {
Log.i("$TAG First time Linphone 6.0 has been started, showing Welcome activity") Log.i("$TAG First time Linphone 6.0 has been started, showing Welcome activity")
@ -361,18 +447,6 @@ class MainActivity : GenericActivity() {
} }
} else { } else {
coreContext.postOnMainThread { 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")) { if (intent.hasExtra("Chat")) {
Log.i("$TAG Intent has [Chat] extra") Log.i("$TAG Intent has [Chat] extra")
try { try {
@ -404,64 +478,12 @@ class MainActivity : GenericActivity() {
} catch (ise: IllegalStateException) { } catch (ise: IllegalStateException) {
Log.e("$TAG Can't navigate to Conversations fragment: $ise") 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) { private fun handleSendIntent(intent: Intent, multiple: Boolean) {
val parcelablesUri = arrayListOf<Uri>() val parcelablesUri = arrayListOf<Uri>()
@ -550,7 +572,6 @@ class MainActivity : GenericActivity() {
} }
} }
@MainThread
private fun parseShortcutIfAny(intent: Intent): Pair<String, String>? { private fun parseShortcutIfAny(intent: Intent): Pair<String, String>? {
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
if (shortcutId != null) { if (shortcutId != null) {
@ -562,7 +583,6 @@ class MainActivity : GenericActivity() {
return null return null
} }
@MainThread
private fun handleCallIntent(intent: Intent) { private fun handleCallIntent(intent: Intent) {
val uri = intent.data?.toString() val uri = intent.data?.toString()
if (uri.isNullOrEmpty()) { if (uri.isNullOrEmpty()) {
@ -595,7 +615,6 @@ class MainActivity : GenericActivity() {
} }
} }
@MainThread
private fun handleConfigIntent(uri: String) { private fun handleConfigIntent(uri: String) {
val remoteConfigUri = uri.substring("linphone-config:".length) val remoteConfigUri = uri.substring("linphone-config:".length)
val url = when { val url = when {

View file

@ -699,6 +699,12 @@ class MessageModel @WorkerThread constructor(
@WorkerThread @WorkerThread
private fun startVoiceRecordPlayer() { private fun startVoiceRecordPlayer() {
if (voiceRecordAudioFocusRequest == null) {
voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
}
if (isPlayerClosed()) { if (isPlayerClosed()) {
Log.w("$TAG Player closed, let's open it first") Log.w("$TAG Player closed, let's open it first")
initVoiceRecordPlayer() initVoiceRecordPlayer()
@ -712,12 +718,6 @@ class MessageModel @WorkerThread constructor(
) )
} }
if (voiceRecordAudioFocusRequest == null) {
voiceRecordAudioFocusRequest = AudioUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
coreContext.context
)
}
Log.i("$TAG Playing voice record") Log.i("$TAG Playing voice record")
isPlayingVoiceRecord.postValue(true) isPlayingVoiceRecord.postValue(true)
voiceRecordPlayer.start() voiceRecordPlayer.start()