mirror of
https://github.com/FossifyOrg/Messages.git
synced 2026-07-31 18:18:24 -04:00
fix: invalidate stale thread participants when recipients change (#812)
* fix: invalidate stale thread participants when recipients change Refs: https://github.com/FossifyOrg/Messages/issues/615 * fix: preserve intent numbers for empty conversation threads
This commit is contained in:
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
- Fixed messages being sent to the wrong contact ([#615])
|
||||
|
||||
### Fixed
|
||||
- Fixed incomplete message exports ([#713])
|
||||
@@ -238,6 +240,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
[#574]: https://github.com/FossifyOrg/Messages/issues/574
|
||||
[#600]: https://github.com/FossifyOrg/Messages/issues/600
|
||||
[#610]: https://github.com/FossifyOrg/Messages/issues/610
|
||||
[#615]: https://github.com/FossifyOrg/Messages/issues/615
|
||||
[#641]: https://github.com/FossifyOrg/Messages/issues/641
|
||||
[#644]: https://github.com/FossifyOrg/Messages/issues/644
|
||||
[#651]: https://github.com/FossifyOrg/Messages/issues/651
|
||||
|
||||
@@ -492,12 +492,13 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val providerParticipantsChanged = reconcileProviderParticipants()
|
||||
val hasParticipantWithoutName = participants.any { contact ->
|
||||
contact.phoneNumbers.map { it.normalizedNumber }.contains(contact.name)
|
||||
}
|
||||
|
||||
try {
|
||||
if (participants.isNotEmpty() && messages.hashCode() == cachedMessagesCode && !hasParticipantWithoutName) {
|
||||
if (canReuseLoadedThread(providerParticipantsChanged, cachedMessagesCode, hasParticipantWithoutName)) {
|
||||
setupAdapter()
|
||||
runOnUiThread { callback() }
|
||||
return@ensureBackgroundThread
|
||||
@@ -564,6 +565,18 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun canReuseLoadedThread(
|
||||
providerParticipantsChanged: Boolean,
|
||||
cachedMessagesCode: Int,
|
||||
hasParticipantWithoutName: Boolean,
|
||||
): Boolean {
|
||||
if (providerParticipantsChanged || participants.isEmpty() || hasParticipantWithoutName) {
|
||||
return false
|
||||
}
|
||||
|
||||
return messages.hashCode() == cachedMessagesCode
|
||||
}
|
||||
|
||||
private fun getOrCreateThreadAdapter(): ThreadAdapter {
|
||||
var currAdapter = binding.threadMessagesList.adapter
|
||||
if (currAdapter == null) {
|
||||
@@ -962,6 +975,32 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasOnlyScheduledMessages(): Boolean {
|
||||
return messages.isNotEmpty() && messages.all { it.isScheduled }
|
||||
}
|
||||
|
||||
private fun getProviderThreadParticipants(): ArrayList<SimpleContact>? {
|
||||
if (isRecycleBin || threadId <= 0L || hasOnlyScheduledMessages()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getThreadParticipants(threadId, null).takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun reconcileProviderParticipants(): Boolean {
|
||||
val providerParticipants = getProviderThreadParticipants() ?: return false
|
||||
val participantsChanged = participants.getAddresses() != providerParticipants.getAddresses()
|
||||
participants = providerParticipants
|
||||
|
||||
if (participantsChanged) {
|
||||
runOnUiThread {
|
||||
maybeDisableShortCodeReply()
|
||||
}
|
||||
}
|
||||
|
||||
return participantsChanged
|
||||
}
|
||||
|
||||
private fun setupParticipants() {
|
||||
if (participants.isEmpty()) {
|
||||
participants = if (messages.isEmpty()) {
|
||||
@@ -969,7 +1008,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
val participants = getThreadParticipants(threadId, null)
|
||||
fixParticipantNumbers(participants, intentNumbers)
|
||||
} else {
|
||||
messages.first().participants
|
||||
getProviderThreadParticipants() ?: messages.first().participants
|
||||
}
|
||||
runOnUiThread {
|
||||
maybeDisableShortCodeReply()
|
||||
@@ -1778,6 +1817,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
|
||||
val providerParticipantsChanged = reconcileProviderParticipants()
|
||||
val newThreadId = getThreadId(participants.getAddresses().toSet())
|
||||
val newMessages = getMessages(newThreadId, includeScheduledMessages = false)
|
||||
if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
|
||||
@@ -1806,6 +1846,9 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
setupAdapter()
|
||||
runOnUiThread {
|
||||
if (providerParticipantsChanged) {
|
||||
setupThreadTitle()
|
||||
}
|
||||
setupSIMSelector()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,61 +618,67 @@ fun Context.getThreadParticipants(
|
||||
threadId: Long,
|
||||
contactsMap: HashMap<Int, SimpleContact>?,
|
||||
): ArrayList<SimpleContact> {
|
||||
MessagingCache.participantsCache.get(threadId)?.let {
|
||||
return it.map { contact ->
|
||||
contact.copy(
|
||||
phoneNumbers = contact.phoneNumbers.toArrayList(),
|
||||
birthdays = contact.birthdays.toArrayList(),
|
||||
anniversaries = contact.anniversaries.toArrayList()
|
||||
)
|
||||
}.toArrayList()
|
||||
val recipientIds = getThreadRecipientIds(threadId)
|
||||
val phoneNumbers = getThreadPhoneNumbers(recipientIds)
|
||||
MessagingCache.participantsCache.get(threadId, phoneNumbers)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val uri = "${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true".toUri()
|
||||
val projection = arrayOf(
|
||||
ThreadsColumns.RECIPIENT_IDS
|
||||
)
|
||||
val selection = "${Mms._ID} = ?"
|
||||
val selectionArgs = arrayOf(threadId.toString())
|
||||
val participants = ArrayList<SimpleContact>()
|
||||
try {
|
||||
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
|
||||
cursor?.use {
|
||||
if (cursor.moveToFirst()) {
|
||||
val address = cursor.getStringValue(ThreadsColumns.RECIPIENT_IDS)
|
||||
address.split(" ").filter { it.areDigitsOnly() }.forEach {
|
||||
val addressId = it.toInt()
|
||||
if (contactsMap?.containsKey(addressId) == true) {
|
||||
participants.add(contactsMap[addressId]!!)
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val number = getPhoneNumberFromAddressId(addressId)
|
||||
val namePhoto = getNameAndPhotoFromPhoneNumber(number)
|
||||
val name = namePhoto.name
|
||||
val photoUri = namePhoto.photoUri ?: ""
|
||||
val phoneNumber = PhoneNumber(number, 0, "", number)
|
||||
val contact = SimpleContact(
|
||||
rawId = addressId,
|
||||
contactId = addressId,
|
||||
name = name,
|
||||
photoUri = photoUri,
|
||||
phoneNumbers = arrayListOf(phoneNumber),
|
||||
birthdays = ArrayList(),
|
||||
anniversaries = ArrayList()
|
||||
)
|
||||
participants.add(contact)
|
||||
}
|
||||
}
|
||||
recipientIds.zip(phoneNumbers).forEach { (addressId, number) ->
|
||||
val cachedContact = contactsMap?.get(addressId)
|
||||
val threadPhoneNumber = cachedContact
|
||||
?.phoneNumbers
|
||||
?.firstOrNull { it.normalizedNumber == number }
|
||||
if (cachedContact != null && threadPhoneNumber != null) {
|
||||
participants.add(
|
||||
cachedContact.copy(
|
||||
phoneNumbers = arrayListOf(threadPhoneNumber.copy()),
|
||||
birthdays = ArrayList(cachedContact.birthdays),
|
||||
anniversaries = ArrayList(cachedContact.anniversaries)
|
||||
)
|
||||
)
|
||||
return@forEach
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
|
||||
val namePhoto = getNameAndPhotoFromPhoneNumber(number)
|
||||
val name = namePhoto.name
|
||||
val photoUri = namePhoto.photoUri ?: ""
|
||||
val phoneNumber = PhoneNumber(number, 0, "", number)
|
||||
val contact = SimpleContact(
|
||||
rawId = addressId,
|
||||
contactId = addressId,
|
||||
name = name,
|
||||
photoUri = photoUri,
|
||||
phoneNumbers = arrayListOf(phoneNumber),
|
||||
birthdays = ArrayList(),
|
||||
anniversaries = ArrayList()
|
||||
)
|
||||
participants.add(contact)
|
||||
}
|
||||
|
||||
MessagingCache.participantsCache.put(threadId, participants)
|
||||
MessagingCache.participantsCache.put(threadId, phoneNumbers, participants)
|
||||
return participants
|
||||
}
|
||||
|
||||
private fun Context.getThreadRecipientIds(threadId: Long): List<Int> {
|
||||
val recipientIds = ArrayList<Int>()
|
||||
queryCursor(
|
||||
uri = "${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true".toUri(),
|
||||
projection = arrayOf(ThreadsColumns.RECIPIENT_IDS),
|
||||
selection = "${Mms._ID} = ?",
|
||||
selectionArgs = arrayOf(threadId.toString()),
|
||||
showErrors = true
|
||||
) { cursor ->
|
||||
cursor.getStringValue(ThreadsColumns.RECIPIENT_IDS)
|
||||
.split(" ")
|
||||
.filter { it.areDigitsOnly() }
|
||||
.forEach { recipientIds.add(it.toInt()) }
|
||||
}
|
||||
|
||||
return recipientIds
|
||||
}
|
||||
|
||||
fun Context.getThreadPhoneNumbers(recipientIds: List<Int>): ArrayList<String> {
|
||||
val numbers = ArrayList<String>()
|
||||
recipientIds.forEach {
|
||||
|
||||
@@ -8,5 +8,71 @@ private const val CACHE_SIZE = 512
|
||||
|
||||
object MessagingCache {
|
||||
val namePhoto = LruCache<String, NamePhoto>(CACHE_SIZE)
|
||||
val participantsCache = LruCache<Long, ArrayList<SimpleContact>>(CACHE_SIZE)
|
||||
internal val participantsCache = ThreadParticipantsCache(CACHE_SIZE)
|
||||
}
|
||||
|
||||
internal class ThreadParticipantsCache(private val maxSize: Int) {
|
||||
private val lock = Any()
|
||||
private val entries = LruCache<Long, Entry>(maxSize)
|
||||
|
||||
fun get(threadId: Long, recipientNumbers: List<String>): ArrayList<SimpleContact>? {
|
||||
val key = recipientNumbers.toRecipientCacheKey()
|
||||
synchronized(lock) {
|
||||
val entry = entries.get(threadId) ?: return null
|
||||
if (entry.recipientNumbers != key) {
|
||||
entries.remove(threadId)
|
||||
return null
|
||||
}
|
||||
|
||||
return entry.participants.deepCopy()
|
||||
}
|
||||
}
|
||||
|
||||
fun put(
|
||||
threadId: Long,
|
||||
recipientNumbers: List<String>,
|
||||
participants: ArrayList<SimpleContact>,
|
||||
) {
|
||||
if (participants.isEmpty()) return
|
||||
synchronized(lock) {
|
||||
entries.put(
|
||||
threadId,
|
||||
Entry(
|
||||
recipientNumbers = recipientNumbers.toRecipientCacheKey(),
|
||||
participants = participants.deepCopy()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(threadId: Long) {
|
||||
synchronized(lock) {
|
||||
entries.remove(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
fun evictAll() {
|
||||
synchronized(lock) {
|
||||
entries.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
private data class Entry(
|
||||
val recipientNumbers: List<String>,
|
||||
val participants: ArrayList<SimpleContact>,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<String>.toRecipientCacheKey(): List<String> {
|
||||
return map { it.trim() }
|
||||
}
|
||||
|
||||
private fun ArrayList<SimpleContact>.deepCopy(): ArrayList<SimpleContact> {
|
||||
return map { contact ->
|
||||
contact.copy(
|
||||
phoneNumbers = ArrayList(contact.phoneNumbers),
|
||||
birthdays = ArrayList(contact.birthdays),
|
||||
anniversaries = ArrayList(contact.anniversaries)
|
||||
)
|
||||
}.toCollection(ArrayList())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user