Added a contacts cache at app level + get notified on low memory and clear glide cache when it happens

This commit is contained in:
Sylvain Berfini 2021-07-15 13:21:00 +02:00
parent 37cb30047e
commit 536e78e98e
12 changed files with 71 additions and 19 deletions

View file

@ -20,7 +20,9 @@
package org.linphone.activities.main package org.linphone.activities.main
import android.app.Activity import android.app.Activity
import android.content.ComponentCallbacks2
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -58,6 +60,7 @@ import org.linphone.databinding.MainActivityBinding
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.GlideApp
class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestinationChangedListener { class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestinationChangedListener {
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
@ -84,6 +87,19 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
private var initPosY = 0f private var initPosY = 0f
private var overlay: View? = null private var overlay: View? = null
private val componentCallbacks = object : ComponentCallbacks2 {
override fun onConfigurationChanged(newConfig: Configuration) { }
override fun onLowMemory() {
Log.w("[Main Activity] onLowMemory !")
}
override fun onTrimMemory(level: Int) {
Log.w("[Main Activity] onTrimMemory called with level $level !")
GlideApp.get(this@MainActivity).clearMemory()
}
}
override fun onLayoutChanges(foldingFeature: FoldingFeature?) { override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
sharedViewModel.layoutChangedEvent.value = Event(true) sharedViewModel.layoutChangedEvent.value = Event(true)
} }
@ -155,6 +171,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
registerComponentCallbacks(componentCallbacks)
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this) findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
if (intent != null) handleIntentParams(intent) if (intent != null) handleIntentParams(intent)
@ -162,6 +179,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onDestroy() { override fun onDestroy() {
findNavController(R.id.nav_host_fragment).removeOnDestinationChangedListener(this) findNavController(R.id.nav_host_fragment).removeOnDestinationChangedListener(this)
unregisterComponentCallbacks(componentCallbacks)
super.onDestroy() super.onDestroy()
} }

View file

@ -120,7 +120,7 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
} }
})*/ })*/
/* End of hared view model & sliding pane related */ /* End of shared view model & sliding pane related */
_adapter = ChatRoomsListAdapter(listSelectionViewModel, viewLifecycleOwner) _adapter = ChatRoomsListAdapter(listSelectionViewModel, viewLifecycleOwner)
// SubmitList is done on a background thread // SubmitList is done on a background thread

View file

@ -108,7 +108,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
} }
})*/ })*/
/* End of hared view model & sliding pane related */ /* End of shared view model & sliding pane related */
_adapter = ContactsListAdapter(listSelectionViewModel, viewLifecycleOwner) _adapter = ContactsListAdapter(listSelectionViewModel, viewLifecycleOwner)
binding.contactsList.setHasFixedSize(true) binding.contactsList.setHasFixedSize(true)

View file

@ -111,7 +111,7 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
} }
})*/ })*/
/* End of hared view model & sliding pane related */ /* End of shared view model & sliding pane related */
_adapter = CallLogsListAdapter(listSelectionViewModel, viewLifecycleOwner) _adapter = CallLogsListAdapter(listSelectionViewModel, viewLifecycleOwner)
// SubmitList is done on a background thread // SubmitList is done on a background thread

View file

@ -97,6 +97,9 @@ class CallLogsListViewModel : ViewModel() {
} }
private fun updateCallLogs() { private fun updateCallLogs() {
callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
missedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
val list = arrayListOf<GroupedCallLogData>() val list = arrayListOf<GroupedCallLogData>()
val missedList = arrayListOf<GroupedCallLogData>() val missedList = arrayListOf<GroupedCallLogData>()

View file

@ -70,7 +70,7 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
}) })
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/* End of hared view model & sliding pane related */ /* End of shared view model & sliding pane related */
viewModel = ViewModelProvider(this).get(SettingsViewModel::class.java) viewModel = ViewModelProvider(this).get(SettingsViewModel::class.java)
binding.viewModel = viewModel binding.viewModel = viewModel

View file

@ -25,6 +25,7 @@ import android.app.Activity
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
@ -63,6 +64,10 @@ class Api21Compatibility {
vibrator.vibrate(pattern, -1) vibrator.vibrate(pattern, -1)
} }
fun getBitmapFromUri(context: Context, uri: Uri): Bitmap {
return MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
}
suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
if (!PermissionHelper.get().hasWriteExternalStorage()) { if (!PermissionHelper.get().hasWriteExternalStorage()) {
Log.e("[Media Store] Write external storage permission denied") Log.e("[Media Store] Write external storage permission denied")

View file

@ -25,6 +25,8 @@ import android.app.NotificationManager
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
@ -83,6 +85,10 @@ class Api29Compatibility {
return bubblesAllowed return bubblesAllowed
} }
fun getBitmapFromUri(context: Context, uri: Uri): Bitmap {
return ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri))
}
suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
if (filePath == null) { if (filePath == null) {

View file

@ -23,6 +23,8 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Vibrator import android.os.Vibrator
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
@ -74,6 +76,14 @@ class Compatibility {
} }
} }
fun getBitmapFromUri(context: Context, uri: Uri): Bitmap {
return if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10)) {
Api21Compatibility.getBitmapFromUri(context, uri)
} else {
Api29Compatibility.getBitmapFromUri(context, uri)
}
}
/* Notifications */ /* Notifications */
fun createNotificationChannels( fun createNotificationChannels(

View file

@ -20,7 +20,7 @@
package org.linphone.contact package org.linphone.contact
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.activities.main.viewmodels.ErrorReportingViewModel import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.ChatRoomSecurityLevel import org.linphone.core.ChatRoomSecurityLevel
@ -50,18 +50,18 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte
init { init {
securityLevel.value = ChatRoomSecurityLevel.ClearText securityLevel.value = ChatRoomSecurityLevel.ClearText
LinphoneApplication.coreContext.contactsManager.addListener(contactsUpdatedListener) coreContext.contactsManager.addListener(contactsUpdatedListener)
contactLookup() contactLookup()
} }
open fun destroy() { open fun destroy() {
LinphoneApplication.coreContext.contactsManager.removeListener(contactsUpdatedListener) coreContext.contactsManager.removeListener(contactsUpdatedListener)
} }
private fun contactLookup() { private fun contactLookup() {
displayName.value = LinphoneUtils.getDisplayName(sipAddress) displayName.value = LinphoneUtils.getDisplayName(sipAddress)
contact.value = contact.value =
LinphoneApplication.coreContext.contactsManager.findContactByAddress(sipAddress) coreContext.contactsManager.findContactByAddress(sipAddress)
} }
} }
@ -78,18 +78,18 @@ abstract class GenericContactViewModel(private val sipAddress: Address) : ErrorR
init { init {
securityLevel.value = ChatRoomSecurityLevel.ClearText securityLevel.value = ChatRoomSecurityLevel.ClearText
LinphoneApplication.coreContext.contactsManager.addListener(contactsUpdatedListener) coreContext.contactsManager.addListener(contactsUpdatedListener)
contactLookup() contactLookup()
} }
override fun onCleared() { override fun onCleared() {
LinphoneApplication.coreContext.contactsManager.removeListener(contactsUpdatedListener) coreContext.contactsManager.removeListener(contactsUpdatedListener)
super.onCleared() super.onCleared()
} }
private fun contactLookup() { private fun contactLookup() {
displayName.value = LinphoneUtils.getDisplayName(sipAddress) displayName.value = LinphoneUtils.getDisplayName(sipAddress)
contact.value = LinphoneApplication.coreContext.contactsManager.findContactByAddress(sipAddress) contact.value = coreContext.contactsManager.findContactByAddress(sipAddress)
} }
} }

View file

@ -93,6 +93,8 @@ class ContactsManager(private val context: Context) {
var latestContactFetch: String = "" var latestContactFetch: String = ""
private val friendsMap: HashMap<String, Friend> = HashMap()
private val contactsUpdatedListeners = ArrayList<ContactsUpdatedListener>() private val contactsUpdatedListeners = ArrayList<ContactsUpdatedListener>()
private var loadContactsTask: AsyncContactsLoader? = null private var loadContactsTask: AsyncContactsLoader? = null
@ -227,7 +229,9 @@ class ContactsManager(private val context: Context) {
@Synchronized @Synchronized
fun findContactByPhoneNumber(number: String): Contact? { fun findContactByPhoneNumber(number: String): Contact? {
val friend: Friend? = coreContext.core.findFriendByPhoneNumber(number) val cacheFriend = friendsMap[number]
val friend: Friend? = cacheFriend ?: coreContext.core.findFriendByPhoneNumber(number)
if (cacheFriend == null && friend != null) friendsMap[number] = friend
return friend?.userData as? Contact return friend?.userData as? Contact
} }
@ -240,8 +244,14 @@ class ContactsManager(private val context: Context) {
} }
if (localContact != null) return localContact if (localContact != null) return localContact
val cleanAddress = address.clone()
cleanAddress.clean() // To remove gruu if any
val cleanStringAddress = cleanAddress.asStringUriOnly()
val cacheFriend = friendsMap[cleanStringAddress]
val friend: Friend? = coreContext.core.findFriend(address) val friend: Friend? = coreContext.core.findFriend(address)
val contact: Contact? = friend?.userData as? Contact val contact: Contact? = friend?.userData as? Contact
if (cacheFriend == null && friend != null) friendsMap[cleanStringAddress] = friend
if (contact != null) return contact if (contact != null) return contact
val username = address.username val username = address.username
@ -283,12 +293,14 @@ class ContactsManager(private val context: Context) {
context.contentResolver.unregisterContentObserver(contactsObserver) context.contentResolver.unregisterContentObserver(contactsObserver)
loadContactsTask?.cancel(true) loadContactsTask?.cancel(true)
friendsMap.clear()
// Contact has a Friend field and Friend can have a Contact has userData // Contact has a Friend field and Friend can have a Contact has userData
// Friend also keeps a ref on the Core, so we have to clean them // Friend also keeps a ref on the Core, so we have to clean them
for (contact in contacts) { for (contact in contacts) {
contact.friend = null contact.friend = null
} }
contacts.clear() contacts.clear()
for (contact in sipContacts) { for (contact in sipContacts) {
contact.friend = null contact.friend = null
} }

View file

@ -27,6 +27,7 @@ import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import java.io.File import java.io.File
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
class ImageUtils { class ImageUtils {
@ -36,22 +37,19 @@ class ImageUtils {
fromPictureUri: Uri? fromPictureUri: Uri?
): Bitmap? { ): Bitmap? {
var bm: Bitmap? = null var bm: Bitmap? = null
val roundBm: Bitmap?
if (fromPictureUri != null) { if (fromPictureUri != null) {
bm = try { bm = try {
@Suppress("DEPRECATION") Compatibility.getBitmapFromUri(context, fromPictureUri)
MediaStore.Images.Media.getBitmap(
context.contentResolver, fromPictureUri
)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("[Image Utils] Failed to get bitmap from URI [$fromPictureUri]: $e")
return null return null
} }
} }
if (bm != null) { if (bm != null) {
roundBm = getRoundBitmap(bm) val roundBm = getRoundBitmap(bm)
if (roundBm != null) { if (roundBm != null) {
bm.recycle() bm.recycle()
bm = roundBm return roundBm
} }
} }
return bm return bm