mirror of
https://github.com/FossifyOrg/Messages.git
synced 2025-12-24 00:00:16 -05:00
perf: optimize loading messages in threads (#552)
* perf: improve lazy loading and remove spinner * perf: optimize message loading by caching and reducing queries * docs: update changelog * style: use constant for cache size * refactor: minor consistency improvement * fix: override loaded preview size * refactor: streamline message loading logic in scroll listener * refactor: organize some dedup related code * build: bump detekt return count limit 2 is 2 low * fix: check contacts permissions before registering observer * fix: disable fetching media resolutions in threads * refactor: remove resolution fetching related code * perf: cache MMS thread participants * refactor: remove unused BitmapFactory import * fix: invalidate participants cache when necessary * fix: return copied participants from cache * fix: adjust image loading dimensions in threads * fix: use stable ids for header items * fix: always rely on database check before flipping `allMessagesFetched`
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]
|
||||
### Changed
|
||||
- Optimized loading messages in conversations
|
||||
|
||||
## [1.4.0] - 2025-10-12
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
package org.fossify.messages
|
||||
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.ContactsContract
|
||||
import org.fossify.commons.FossifyApp
|
||||
import org.fossify.commons.extensions.hasPermission
|
||||
import org.fossify.commons.helpers.PERMISSION_READ_CONTACTS
|
||||
import org.fossify.messages.helpers.MessagingCache
|
||||
|
||||
class App : FossifyApp() {
|
||||
override val isAppLockFeatureAvailable = true
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (hasPermission(PERMISSION_READ_CONTACTS)) {
|
||||
listOf(
|
||||
ContactsContract.Contacts.CONTENT_URI,
|
||||
ContactsContract.Data.CONTENT_URI,
|
||||
ContactsContract.DisplayPhoto.CONTENT_URI
|
||||
).forEach {
|
||||
try {
|
||||
contentResolver.registerContentObserver(it, true, contactsObserver)
|
||||
} catch (_: Exception){
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||
MessagingCache.namePhoto.evictAll()
|
||||
MessagingCache.participantsCache.evictAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,11 +380,7 @@ class MainActivity : SimpleActivity() {
|
||||
|
||||
if (config.appRunCount == 1) {
|
||||
conversations.map { it.threadId }.forEach { threadId ->
|
||||
val messages = getMessages(
|
||||
threadId = threadId,
|
||||
getImageResolutions = false,
|
||||
includeScheduledMessages = false
|
||||
)
|
||||
val messages = getMessages(threadId, includeScheduledMessages = false)
|
||||
messages.chunked(30).forEach { currentMessages ->
|
||||
messagesDB.insertMessages(*currentMessages.toTypedArray())
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import android.content.Intent
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
@@ -49,7 +47,6 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
@@ -87,7 +84,6 @@ import org.fossify.commons.extensions.openRequestExactAlarmSettings
|
||||
import org.fossify.commons.extensions.realScreenSize
|
||||
import org.fossify.commons.extensions.showErrorToast
|
||||
import org.fossify.commons.extensions.showKeyboard
|
||||
import org.fossify.commons.extensions.toInt
|
||||
import org.fossify.commons.extensions.toast
|
||||
import org.fossify.commons.extensions.updateTextColors
|
||||
import org.fossify.commons.extensions.value
|
||||
@@ -105,7 +101,6 @@ import org.fossify.commons.helpers.isSPlus
|
||||
import org.fossify.commons.models.PhoneNumber
|
||||
import org.fossify.commons.models.RadioItem
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
import org.fossify.messages.BuildConfig
|
||||
import org.fossify.messages.R
|
||||
import org.fossify.messages.adapters.AttachmentsAdapter
|
||||
@@ -143,6 +138,7 @@ import org.fossify.messages.extensions.markMessageRead
|
||||
import org.fossify.messages.extensions.markThreadMessagesUnread
|
||||
import org.fossify.messages.extensions.messagesDB
|
||||
import org.fossify.messages.extensions.moveMessageToRecycleBin
|
||||
import org.fossify.messages.extensions.onScroll
|
||||
import org.fossify.messages.extensions.removeDiacriticsIfNeeded
|
||||
import org.fossify.messages.extensions.renameConversation
|
||||
import org.fossify.messages.extensions.restoreAllMessagesFromRecycleBinForConversation
|
||||
@@ -192,7 +188,6 @@ import org.fossify.messages.models.SIMCard
|
||||
import org.fossify.messages.models.ThreadItem
|
||||
import org.fossify.messages.models.ThreadItem.ThreadDateTime
|
||||
import org.fossify.messages.models.ThreadItem.ThreadError
|
||||
import org.fossify.messages.models.ThreadItem.ThreadLoading
|
||||
import org.fossify.messages.models.ThreadItem.ThreadSending
|
||||
import org.fossify.messages.models.ThreadItem.ThreadSent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -200,16 +195,11 @@ import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.joda.time.DateTime
|
||||
import java.io.File
|
||||
import androidx.core.net.toUri
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.fossify.messages.extensions.filterNotInByKey
|
||||
|
||||
class ThreadActivity : SimpleActivity() {
|
||||
private val MIN_DATE_TIME_DIFF_SECS = 300
|
||||
|
||||
private val TYPE_EDIT = 14
|
||||
private val TYPE_SEND = 15
|
||||
private val TYPE_DELETE = 16
|
||||
|
||||
private val SCROLL_TO_BOTTOM_FAB_LIMIT = 20
|
||||
|
||||
private var threadId = 0L
|
||||
private var currentSIMCardIndex = 0
|
||||
private var isActivityVisible = false
|
||||
@@ -489,12 +479,10 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
val cachedMessagesCode = messages.clone().hashCode()
|
||||
if (!isRecycleBin) {
|
||||
messages = getMessages(threadId, true)
|
||||
messages = getMessages(threadId)
|
||||
if (config.useRecycleBin) {
|
||||
val recycledMessages =
|
||||
messagesDB.getThreadMessagesFromRecycleBin(threadId).map { it.id }
|
||||
messages = messages.filter { !recycledMessages.contains(it.id) }
|
||||
.toMutableList() as ArrayList<Message>
|
||||
val recycledMessages = messagesDB.getThreadMessagesFromRecycleBin(threadId)
|
||||
messages = messages.filterNotInByKey(recycledMessages) { it.getStableId() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,7 +548,6 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
setupAttachmentSizes()
|
||||
setupAdapter()
|
||||
runOnUiThread {
|
||||
setupThreadTitle()
|
||||
@@ -587,14 +574,6 @@ class ThreadActivity : SimpleActivity() {
|
||||
)
|
||||
|
||||
binding.threadMessagesList.adapter = currAdapter
|
||||
binding.threadMessagesList.endlessScrollListener =
|
||||
object : MyRecyclerView.EndlessScrollListener {
|
||||
override fun updateBottom() {}
|
||||
|
||||
override fun updateTop() {
|
||||
fetchNextMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
return currAdapter as ThreadAdapter
|
||||
}
|
||||
@@ -663,21 +642,21 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupScrollFab() {
|
||||
binding.threadMessagesList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
private fun setupScrollListener() {
|
||||
binding.threadMessagesList.onScroll(
|
||||
onScrolled = { dx, dy ->
|
||||
tryLoadMoreMessages()
|
||||
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
|
||||
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
val isCloseToBottom =
|
||||
lastVisibleItemPosition >= getOrCreateThreadAdapter().itemCount - SCROLL_TO_BOTTOM_FAB_LIMIT
|
||||
if (isCloseToBottom) {
|
||||
binding.scrollToBottomFab.hide()
|
||||
} else {
|
||||
binding.scrollToBottomFab.show()
|
||||
}
|
||||
val fab = binding.scrollToBottomFab
|
||||
if (isCloseToBottom) fab.hide() else fab.show()
|
||||
},
|
||||
onScrollStateChanged = { newState ->
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) tryLoadMoreMessages()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleItemClick(any: Any) {
|
||||
@@ -737,35 +716,25 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchNextMessages() {
|
||||
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) {
|
||||
if (allMessagesFetched) {
|
||||
getOrCreateThreadAdapter().apply {
|
||||
val newList = currentList.toMutableList().apply {
|
||||
removeAll { it is ThreadLoading }
|
||||
}
|
||||
updateMessages(
|
||||
newMessages = newList as ArrayList<ThreadItem>,
|
||||
scrollPosition = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
private fun tryLoadMoreMessages() {
|
||||
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
|
||||
if (layoutManager.findFirstVisibleItemPosition() <= PREFETCH_THRESHOLD) {
|
||||
loadMoreMessages()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreMessages() {
|
||||
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) return
|
||||
|
||||
val firstItem = messages.first()
|
||||
val dateOfFirstItem = firstItem.date
|
||||
if (oldestMessageDate == dateOfFirstItem) {
|
||||
allMessagesFetched = true
|
||||
return
|
||||
}
|
||||
|
||||
oldestMessageDate = dateOfFirstItem
|
||||
loadingOlderMessages = true
|
||||
|
||||
ensureBackgroundThread {
|
||||
val olderMessages = getMessages(threadId, true, oldestMessageDate)
|
||||
.filter { message -> !messages.contains(message) }
|
||||
val olderMessages = getMessages(threadId, oldestMessageDate)
|
||||
.filterNotInByKey(messages) { it.getStableId() }
|
||||
|
||||
messages.addAll(0, olderMessages)
|
||||
allMessagesFetched = olderMessages.isEmpty()
|
||||
@@ -773,8 +742,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
runOnUiThread {
|
||||
loadingOlderMessages = false
|
||||
val itemAtRefreshIndex = threadItems.indexOfFirst { it == firstItem }
|
||||
getOrCreateThreadAdapter().updateMessages(threadItems, itemAtRefreshIndex)
|
||||
getOrCreateThreadAdapter().updateMessages(threadItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,7 +764,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
setupThread()
|
||||
setupScrollFab()
|
||||
setupScrollListener()
|
||||
}
|
||||
} else {
|
||||
finish()
|
||||
@@ -913,7 +881,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
if (intent.extras?.containsKey(THREAD_ATTACHMENT_URI) == true) {
|
||||
val uri = Uri.parse(intent.getStringExtra(THREAD_ATTACHMENT_URI))
|
||||
val uri = intent.getStringExtra(THREAD_ATTACHMENT_URI)!!.toUri()
|
||||
addAttachment(uri)
|
||||
} else if (intent.extras?.containsKey(THREAD_ATTACHMENT_URIS) == true) {
|
||||
(intent.getSerializableExtra(THREAD_ATTACHMENT_URIS) as? ArrayList<Uri>)?.forEach {
|
||||
@@ -949,44 +917,6 @@ class ThreadActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAttachmentSizes() {
|
||||
messages.filter { it.attachment != null }.forEach { message ->
|
||||
message.attachment!!.attachments.forEach {
|
||||
try {
|
||||
if (it.mimetype.startsWith("image/")) {
|
||||
val fileOptions = BitmapFactory.Options()
|
||||
fileOptions.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(
|
||||
contentResolver.openInputStream(it.getUri()),
|
||||
null,
|
||||
fileOptions
|
||||
)
|
||||
it.width = fileOptions.outWidth
|
||||
it.height = fileOptions.outHeight
|
||||
} else if (it.mimetype.startsWith("video/")) {
|
||||
val metaRetriever = MediaMetadataRetriever()
|
||||
metaRetriever.setDataSource(this, it.getUri())
|
||||
it.width = metaRetriever.extractMetadata(
|
||||
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH
|
||||
)!!.toInt()
|
||||
it.height = metaRetriever.extractMetadata(
|
||||
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT
|
||||
)!!.toInt()
|
||||
}
|
||||
|
||||
if (it.width < 0) {
|
||||
it.width = 0
|
||||
}
|
||||
|
||||
if (it.height < 0) {
|
||||
it.height = 0
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupParticipants() {
|
||||
if (participants.isEmpty()) {
|
||||
participants = if (messages.isEmpty()) {
|
||||
@@ -1346,11 +1276,6 @@ class ThreadActivity : SimpleActivity() {
|
||||
bus?.post(Events.RefreshMessages())
|
||||
}
|
||||
|
||||
if (!allMessagesFetched && messages.size >= MESSAGES_LIMIT) {
|
||||
val threadLoading = ThreadLoading(generateRandomId())
|
||||
items.add(0, threadLoading)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -1615,13 +1540,8 @@ class ThreadActivity : SimpleActivity() {
|
||||
refreshedSinceSent = false
|
||||
sendMessageCompat(text, addresses, subscriptionId, attachments, messageToResend)
|
||||
ensureBackgroundThread {
|
||||
val messageIds = messages.map { it.id }
|
||||
val messages = getMessages(
|
||||
threadId = threadId,
|
||||
getImageResolutions = true,
|
||||
limit = maxOf(1, attachments.size)
|
||||
)
|
||||
.filter { it.id !in messageIds }
|
||||
val messages = getMessages(threadId, limit = maxOf(1, attachments.size))
|
||||
.filterNotInByKey(messages) { it.getStableId() }
|
||||
for (message in messages) {
|
||||
insertOrUpdateMessage(message)
|
||||
}
|
||||
@@ -1805,9 +1725,7 @@ class ThreadActivity : SimpleActivity() {
|
||||
|
||||
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
|
||||
val newThreadId = getThreadId(participants.getAddresses().toSet())
|
||||
val newMessages =
|
||||
getMessages(newThreadId, getImageResolutions = true, includeScheduledMessages = false)
|
||||
|
||||
val newMessages = getMessages(newThreadId, includeScheduledMessages = false)
|
||||
if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
|
||||
// update scheduled messages with real thread id
|
||||
threadId = newThreadId
|
||||
@@ -2145,4 +2063,13 @@ class ThreadActivity : SimpleActivity() {
|
||||
} else {
|
||||
getBottomNavigationBackgroundColor()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TYPE_EDIT = 14
|
||||
private const val TYPE_SEND = 15
|
||||
private const val TYPE_DELETE = 16
|
||||
private const val MIN_DATE_TIME_DIFF_SECS = 300
|
||||
private const val SCROLL_TO_BOTTOM_FAB_LIMIT = 20
|
||||
private const val PREFETCH_THRESHOLD = 50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Size
|
||||
import android.util.TypedValue
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
@@ -14,21 +13,34 @@ import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.extensions.applyColorFilter
|
||||
import org.fossify.commons.extensions.beGone
|
||||
import org.fossify.commons.extensions.beVisible
|
||||
import org.fossify.commons.extensions.beVisibleIf
|
||||
import org.fossify.commons.extensions.copyToClipboard
|
||||
import org.fossify.commons.extensions.formatDateOrTime
|
||||
import org.fossify.commons.extensions.getContrastColor
|
||||
import org.fossify.commons.extensions.getProperPrimaryColor
|
||||
import org.fossify.commons.extensions.getTextSize
|
||||
import org.fossify.commons.extensions.shareTextIntent
|
||||
import org.fossify.commons.extensions.showErrorToast
|
||||
import org.fossify.commons.extensions.usableScreenSize
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||
import org.fossify.commons.views.MyRecyclerView
|
||||
@@ -37,17 +49,42 @@ import org.fossify.messages.activities.NewConversationActivity
|
||||
import org.fossify.messages.activities.SimpleActivity
|
||||
import org.fossify.messages.activities.ThreadActivity
|
||||
import org.fossify.messages.activities.VCardViewerActivity
|
||||
import org.fossify.messages.databinding.*
|
||||
import org.fossify.messages.databinding.ItemAttachmentDocumentBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentImageBinding
|
||||
import org.fossify.messages.databinding.ItemAttachmentVcardBinding
|
||||
import org.fossify.messages.databinding.ItemMessageBinding
|
||||
import org.fossify.messages.databinding.ItemThreadDateTimeBinding
|
||||
import org.fossify.messages.databinding.ItemThreadErrorBinding
|
||||
import org.fossify.messages.databinding.ItemThreadSendingBinding
|
||||
import org.fossify.messages.databinding.ItemThreadSuccessBinding
|
||||
import org.fossify.messages.dialogs.DeleteConfirmationDialog
|
||||
import org.fossify.messages.dialogs.MessageDetailsDialog
|
||||
import org.fossify.messages.dialogs.SelectTextDialog
|
||||
import org.fossify.messages.extensions.*
|
||||
import org.fossify.messages.helpers.*
|
||||
import org.fossify.messages.extensions.config
|
||||
import org.fossify.messages.extensions.getContactFromAddress
|
||||
import org.fossify.messages.extensions.isImageMimeType
|
||||
import org.fossify.messages.extensions.isVCardMimeType
|
||||
import org.fossify.messages.extensions.isVideoMimeType
|
||||
import org.fossify.messages.extensions.launchViewIntent
|
||||
import org.fossify.messages.extensions.startContactDetailsIntent
|
||||
import org.fossify.messages.extensions.subscriptionManagerCompat
|
||||
import org.fossify.messages.helpers.EXTRA_VCARD_URI
|
||||
import org.fossify.messages.helpers.THREAD_DATE_TIME
|
||||
import org.fossify.messages.helpers.THREAD_RECEIVED_MESSAGE
|
||||
import org.fossify.messages.helpers.THREAD_SENT_MESSAGE
|
||||
import org.fossify.messages.helpers.THREAD_SENT_MESSAGE_ERROR
|
||||
import org.fossify.messages.helpers.THREAD_SENT_MESSAGE_SENDING
|
||||
import org.fossify.messages.helpers.THREAD_SENT_MESSAGE_SENT
|
||||
import org.fossify.messages.helpers.generateStableId
|
||||
import org.fossify.messages.helpers.setupDocumentPreview
|
||||
import org.fossify.messages.helpers.setupVCardPreview
|
||||
import org.fossify.messages.models.Attachment
|
||||
import org.fossify.messages.models.Message
|
||||
import org.fossify.messages.models.ThreadItem
|
||||
import org.fossify.messages.models.ThreadItem.*
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import org.fossify.messages.models.ThreadItem.ThreadDateTime
|
||||
import org.fossify.messages.models.ThreadItem.ThreadError
|
||||
import org.fossify.messages.models.ThreadItem.ThreadSending
|
||||
import org.fossify.messages.models.ThreadItem.ThreadSent
|
||||
|
||||
class ThreadAdapter(
|
||||
activity: SimpleActivity,
|
||||
@@ -60,11 +97,18 @@ class ThreadAdapter(
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val hasMultipleSIMCards = (activity.subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1
|
||||
private val maxChatBubbleWidth = activity.usableScreenSize.x * 0.8f
|
||||
private val maxChatBubbleWidth = (activity.usableScreenSize.x * 0.8f).toInt()
|
||||
|
||||
companion object {
|
||||
private const val MAX_MEDIA_HEIGHT_RATIO = 3
|
||||
private const val SIM_BITS = 21
|
||||
private const val SIM_MASK = (1L shl SIM_BITS) - 1
|
||||
}
|
||||
|
||||
init {
|
||||
setupDragListener(true)
|
||||
setHasStableIds(true)
|
||||
(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
override fun getActionMenuId() = R.menu.cab_thread
|
||||
@@ -110,9 +154,13 @@ class ThreadAdapter(
|
||||
|
||||
override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position)
|
||||
|
||||
override fun getItemSelectionKey(position: Int) = (currentList.getOrNull(position) as? Message)?.hashCode()
|
||||
override fun getItemSelectionKey(position: Int): Int? {
|
||||
return (currentList.getOrNull(position) as? Message)?.getSelectionKey()
|
||||
}
|
||||
|
||||
override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { (it as? Message)?.hashCode() == key }
|
||||
override fun getItemKeyPosition(key: Int): Int {
|
||||
return currentList.indexOfFirst { (it as? Message)?.getSelectionKey() == key }
|
||||
}
|
||||
|
||||
override fun onActionModeCreated() {}
|
||||
|
||||
@@ -120,7 +168,6 @@ class ThreadAdapter(
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = when (viewType) {
|
||||
THREAD_LOADING -> ItemThreadLoadingBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_DATE_TIME -> ItemThreadDateTimeBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_SENT_MESSAGE_ERROR -> ItemThreadErrorBinding.inflate(layoutInflater, parent, false)
|
||||
THREAD_SENT_MESSAGE_SENT -> ItemThreadSuccessBinding.inflate(layoutInflater, parent, false)
|
||||
@@ -137,7 +184,6 @@ class ThreadAdapter(
|
||||
val isLongClickable = item is Message
|
||||
holder.bindView(item, isClickable, isLongClickable) { itemView, _ ->
|
||||
when (item) {
|
||||
is ThreadLoading -> setupThreadLoading(itemView)
|
||||
is ThreadDateTime -> setupDateTime(itemView, item)
|
||||
is ThreadError -> setupThreadError(itemView)
|
||||
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
|
||||
@@ -150,14 +196,20 @@ class ThreadAdapter(
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return when (val item = getItem(position)) {
|
||||
is Message -> Message.getStableId(item)
|
||||
else -> item.hashCode().toLong()
|
||||
is Message -> item.getStableId()
|
||||
is ThreadDateTime -> {
|
||||
val sim = (item.simID.hashCode().toLong() and SIM_MASK)
|
||||
val key = (item.date.toLong() shl SIM_BITS) or sim
|
||||
generateStableId(THREAD_DATE_TIME, key)
|
||||
}
|
||||
is ThreadError -> generateStableId(THREAD_SENT_MESSAGE_ERROR, item.messageId)
|
||||
is ThreadSending -> generateStableId(THREAD_SENT_MESSAGE_SENDING, item.messageId)
|
||||
is ThreadSent -> generateStableId(THREAD_SENT_MESSAGE_SENT, item.messageId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (val item = getItem(position)) {
|
||||
is ThreadLoading -> THREAD_LOADING
|
||||
is ThreadDateTime -> THREAD_DATE_TIME
|
||||
is ThreadError -> THREAD_SENT_MESSAGE_ERROR
|
||||
is ThreadSent -> THREAD_SENT_MESSAGE_SENT
|
||||
@@ -268,7 +320,11 @@ class ThreadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedItems() = currentList.filter { selectedKeys.contains((it as? Message)?.hashCode() ?: 0) } as ArrayList<ThreadItem>
|
||||
private fun getSelectedItems(): ArrayList<ThreadItem> {
|
||||
return currentList.filter {
|
||||
selectedKeys.contains((it as? Message)?.getSelectionKey() ?: 0)
|
||||
} as ArrayList<ThreadItem>
|
||||
}
|
||||
|
||||
private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime
|
||||
|
||||
@@ -283,7 +339,7 @@ class ThreadAdapter(
|
||||
|
||||
private fun setupView(holder: ViewHolder, view: View, message: Message) {
|
||||
ItemMessageBinding.bind(view).apply {
|
||||
threadMessageHolder.isSelected = selectedKeys.contains(message.hashCode())
|
||||
threadMessageHolder.isSelected = selectedKeys.contains(message.getSelectionKey())
|
||||
threadMessageBody.apply {
|
||||
text = message.body
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||
@@ -416,16 +472,17 @@ class ThreadAdapter(
|
||||
threadMessageAttachmentsHolder.addView(imageView.root)
|
||||
|
||||
val placeholderDrawable = Color.TRANSPARENT.toDrawable()
|
||||
val isTallImage = attachment.height > attachment.width
|
||||
val transformation = if (isTallImage) CenterCrop() else FitCenter()
|
||||
val options = RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.placeholder(placeholderDrawable)
|
||||
.transform(transformation)
|
||||
.transform(FitCenter())
|
||||
|
||||
var builder = Glide.with(root.context)
|
||||
Glide.with(root.context)
|
||||
.load(uri)
|
||||
.apply(options)
|
||||
.dontAnimate()
|
||||
.override(maxChatBubbleWidth, maxChatBubbleWidth * MAX_MEDIA_HEIGHT_RATIO)
|
||||
.downsample(DownsampleStrategy.AT_MOST)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||
threadMessagePlayOutline.beGone()
|
||||
@@ -435,23 +492,11 @@ class ThreadAdapter(
|
||||
|
||||
override fun onResourceReady(dr: Drawable, a: Any, t: Target<Drawable>, d: DataSource, i: Boolean) = false
|
||||
})
|
||||
.into(imageView.attachmentImage)
|
||||
|
||||
// limit attachment sizes to avoid causing OOM
|
||||
var wantedAttachmentSize = Size(attachment.width, attachment.height)
|
||||
if (wantedAttachmentSize.width > maxChatBubbleWidth) {
|
||||
val newHeight = wantedAttachmentSize.height / (wantedAttachmentSize.width / maxChatBubbleWidth)
|
||||
wantedAttachmentSize = Size(maxChatBubbleWidth.toInt(), newHeight.toInt())
|
||||
}
|
||||
|
||||
builder = if (isTallImage) {
|
||||
builder.override(wantedAttachmentSize.width, wantedAttachmentSize.width)
|
||||
} else {
|
||||
builder.override(wantedAttachmentSize.width, wantedAttachmentSize.height)
|
||||
}
|
||||
|
||||
try {
|
||||
builder.into(imageView.attachmentImage)
|
||||
} catch (_: Exception) {
|
||||
imageView.attachmentImage.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
width = maxChatBubbleWidth
|
||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
|
||||
imageView.attachmentImage.setOnClickListener {
|
||||
@@ -553,11 +598,6 @@ class ThreadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupThreadLoading(view: View) {
|
||||
val binding = ItemThreadLoadingBinding.bind(view)
|
||||
binding.threadLoading.setIndicatorColor(properPrimaryColor)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||
@@ -576,19 +616,21 @@ private class ThreadItemDiffCallback : DiffUtil.ItemCallback<ThreadItem>() {
|
||||
override fun areItemsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean {
|
||||
if (oldItem::class.java != newItem::class.java) return false
|
||||
return when (oldItem) {
|
||||
is ThreadLoading -> oldItem.id == (newItem as ThreadLoading).id
|
||||
is ThreadDateTime -> oldItem.date == (newItem as ThreadDateTime).date
|
||||
is ThreadError -> oldItem.messageId == (newItem as ThreadError).messageId
|
||||
is ThreadSent -> oldItem.messageId == (newItem as ThreadSent).messageId
|
||||
is ThreadSending -> oldItem.messageId == (newItem as ThreadSending).messageId
|
||||
is Message -> Message.areItemsTheSame(oldItem, newItem as Message)
|
||||
is ThreadDateTime -> {
|
||||
val new = newItem as ThreadDateTime
|
||||
oldItem.date == new.date && oldItem.simID == new.simID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean {
|
||||
if (oldItem::class.java != newItem::class.java) return false
|
||||
return when (oldItem) {
|
||||
is ThreadLoading, is ThreadSending -> true
|
||||
is ThreadSending -> true
|
||||
is ThreadDateTime -> oldItem.simID == (newItem as ThreadDateTime).simID
|
||||
is ThreadError -> oldItem.messageText == (newItem as ThreadError).messageText
|
||||
is ThreadSent -> oldItem.delivered == (newItem as ThreadSent).delivered
|
||||
|
||||
@@ -32,3 +32,20 @@ fun Map<String, Any>.toContentValues(): ContentValues {
|
||||
}
|
||||
|
||||
fun <T> Collection<T>.toArrayList() = ArrayList(this)
|
||||
|
||||
inline fun <T> Collection<T>.filterNotInByKey(
|
||||
existing: List<T>,
|
||||
crossinline key: (T) -> Long
|
||||
): ArrayList<T> {
|
||||
if (isEmpty()) return arrayListOf()
|
||||
if (existing.isEmpty()) {
|
||||
return ArrayList(this)
|
||||
}
|
||||
|
||||
val seen = HashSet<Long>(existing.size * 2)
|
||||
for (item in existing) {
|
||||
seen.add(key(item))
|
||||
}
|
||||
|
||||
return filter { seen.add(key(it)) }.toArrayList()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -57,6 +56,7 @@ import org.fossify.messages.helpers.Config
|
||||
import org.fossify.messages.helpers.FILE_SIZE_NONE
|
||||
import org.fossify.messages.helpers.MAX_MESSAGE_LENGTH
|
||||
import org.fossify.messages.helpers.MESSAGES_LIMIT
|
||||
import org.fossify.messages.helpers.MessagingCache
|
||||
import org.fossify.messages.helpers.NotificationHelper
|
||||
import org.fossify.messages.helpers.ShortcutHelper
|
||||
import org.fossify.messages.helpers.generateRandomId
|
||||
@@ -111,7 +111,6 @@ val Context.shortcutHelper get() = ShortcutHelper(this)
|
||||
|
||||
fun Context.getMessages(
|
||||
threadId: Long,
|
||||
getImageResolutions: Boolean,
|
||||
dateFrom: Int = -1,
|
||||
includeScheduledMessages: Boolean = true,
|
||||
limit: Int = MESSAGES_LIMIT,
|
||||
@@ -139,15 +138,7 @@ fun Context.getMessages(
|
||||
var messages = ArrayList<Message>()
|
||||
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
||||
val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor
|
||||
|
||||
val isNumberBlocked = if (blockStatus.containsKey(senderNumber)) {
|
||||
blockStatus[senderNumber]!!
|
||||
} else {
|
||||
val isBlocked = isNumberBlocked(senderNumber, blockedNumbers)
|
||||
blockStatus[senderNumber] = isBlocked
|
||||
isBlocked
|
||||
}
|
||||
|
||||
val isNumberBlocked = blockStatus.getOrPut(senderNumber) { isNumberBlocked(senderNumber, blockedNumbers) }
|
||||
if (isNumberBlocked) {
|
||||
return@queryCursor
|
||||
}
|
||||
@@ -201,7 +192,7 @@ fun Context.getMessages(
|
||||
messages.add(message)
|
||||
}
|
||||
|
||||
messages.addAll(getMMS(threadId, getImageResolutions, sortOrder, dateFrom))
|
||||
messages.addAll(getMMS(threadId, sortOrder, dateFrom))
|
||||
|
||||
if (includeScheduledMessages) {
|
||||
try {
|
||||
@@ -225,7 +216,6 @@ fun Context.getMessages(
|
||||
// as soon as a message contains multiple recipients it counts as an MMS instead of SMS
|
||||
fun Context.getMMS(
|
||||
threadId: Long? = null,
|
||||
getImageResolutions: Boolean = false,
|
||||
sortOrder: String? = null,
|
||||
dateFrom: Int = -1,
|
||||
): ArrayList<Message> {
|
||||
@@ -256,7 +246,6 @@ fun Context.getMMS(
|
||||
|
||||
val messages = ArrayList<Message>()
|
||||
val contactsMap = HashMap<Int, SimpleContact>()
|
||||
val threadParticipants = HashMap<Long, ArrayList<SimpleContact>>()
|
||||
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
||||
val mmsId = cursor.getLongValue(Mms._ID)
|
||||
val type = cursor.getIntValue(Mms.MESSAGE_BOX)
|
||||
@@ -265,16 +254,10 @@ fun Context.getMMS(
|
||||
val threadId = cursor.getLongValue(Mms.THREAD_ID)
|
||||
val subscriptionId = cursor.getIntValue(Mms.SUBSCRIPTION_ID)
|
||||
val status = cursor.getIntValue(Mms.STATUS)
|
||||
val participants = if (threadParticipants.containsKey(threadId)) {
|
||||
threadParticipants[threadId]!!
|
||||
} else {
|
||||
val parts = getThreadParticipants(threadId, contactsMap)
|
||||
threadParticipants[threadId] = parts
|
||||
parts
|
||||
}
|
||||
val participants = getThreadParticipants(threadId, contactsMap)
|
||||
|
||||
val isMMS = true
|
||||
val attachment = getMmsAttachment(mmsId, getImageResolutions)
|
||||
val attachment = getMmsAttachment(mmsId)
|
||||
val body = attachment.text
|
||||
var senderNumber = ""
|
||||
var senderName = ""
|
||||
@@ -478,7 +461,7 @@ fun Context.getConversationIds(): List<Long> {
|
||||
|
||||
// based on https://stackoverflow.com/a/6446831/1967672
|
||||
@SuppressLint("NewApi")
|
||||
fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAttachment {
|
||||
fun Context.getMmsAttachment(id: Long): MessageAttachment {
|
||||
val uri = if (isQPlus()) {
|
||||
Mms.Part.CONTENT_URI
|
||||
} else {
|
||||
@@ -506,32 +489,14 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
|
||||
.orEmpty()
|
||||
} else if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
|
||||
val fileUri = Uri.withAppendedPath(uri, partId.toString())
|
||||
var width = 0
|
||||
var height = 0
|
||||
|
||||
if (getImageResolutions) {
|
||||
try {
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(
|
||||
contentResolver.openInputStream(fileUri),
|
||||
null,
|
||||
options
|
||||
)
|
||||
width = options.outWidth
|
||||
height = options.outHeight
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
messageAttachment.attachments.add(
|
||||
Attachment(
|
||||
id = partId,
|
||||
messageId = id,
|
||||
uriString = fileUri.toString(),
|
||||
mimetype = mimetype,
|
||||
width = width,
|
||||
height = height,
|
||||
width = 0,
|
||||
height = 0,
|
||||
filename = ""
|
||||
)
|
||||
)
|
||||
@@ -569,7 +534,7 @@ fun Context.getLatestMMS(): Message? {
|
||||
|
||||
fun Context.getThreadSnippet(threadId: Long): String {
|
||||
val sortOrder = "${Mms.DATE} DESC LIMIT 1"
|
||||
val latestMms = getMMS(threadId, false, sortOrder).firstOrNull()
|
||||
val latestMms = getMMS(threadId, sortOrder).firstOrNull()
|
||||
var snippet = latestMms?.body ?: ""
|
||||
|
||||
val uri = Sms.CONTENT_URI
|
||||
@@ -620,6 +585,16 @@ 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 uri = "${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true".toUri()
|
||||
val projection = arrayOf(
|
||||
ThreadsColumns.RECIPIENT_IDS
|
||||
@@ -660,6 +635,8 @@ fun Context.getThreadParticipants(
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
}
|
||||
|
||||
MessagingCache.participantsCache.put(threadId, participants)
|
||||
return participants
|
||||
}
|
||||
|
||||
@@ -768,6 +745,7 @@ fun Context.getSuggestedContacts(
|
||||
}
|
||||
|
||||
fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
|
||||
MessagingCache.namePhoto.get(number)?.let { return it }
|
||||
if (!hasPermission(PERMISSION_READ_CONTACTS)) {
|
||||
return NamePhoto(number, null)
|
||||
}
|
||||
@@ -778,19 +756,23 @@ fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
|
||||
PhoneLookup.PHOTO_URI
|
||||
)
|
||||
|
||||
try {
|
||||
val result = try {
|
||||
val cursor = contentResolver.query(uri, projection, null, null, null)
|
||||
cursor.use {
|
||||
if (cursor?.moveToFirst() == true) {
|
||||
val name = cursor.getStringValue(PhoneLookup.DISPLAY_NAME)
|
||||
val photoUri = cursor.getStringValue(PhoneLookup.PHOTO_URI)
|
||||
return NamePhoto(name, photoUri)
|
||||
NamePhoto(name, photoUri)
|
||||
} else {
|
||||
NamePhoto(number, null)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
NamePhoto(number, null)
|
||||
}
|
||||
|
||||
return NamePhoto(number, null)
|
||||
MessagingCache.namePhoto.put(number, result)
|
||||
return result
|
||||
}
|
||||
|
||||
fun Context.insertNewSMS(
|
||||
@@ -856,6 +838,7 @@ fun Context.deleteConversation(threadId: Long) {
|
||||
|
||||
conversationsDB.deleteThreadId(threadId)
|
||||
messagesDB.deleteThreadMessages(threadId)
|
||||
MessagingCache.participantsCache.remove(threadId)
|
||||
|
||||
if (config.customNotifications.contains(threadId.toString())) {
|
||||
config.removeCustomNotificationsByThreadId(threadId)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.fossify.messages.extensions
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
fun RecyclerView.onScroll(
|
||||
onScrolled: ((dx: Int, dy: Int) -> Unit),
|
||||
onScrollStateChanged: ((newState: Int) -> Unit)
|
||||
) {
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
onScrolled.invoke(dx, dy)
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
onScrollStateChanged.invoke(newState)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -60,7 +60,10 @@ const val THREAD_SENT_MESSAGE = 3
|
||||
const val THREAD_SENT_MESSAGE_ERROR = 4
|
||||
const val THREAD_SENT_MESSAGE_SENT = 5
|
||||
const val THREAD_SENT_MESSAGE_SENDING = 6
|
||||
const val THREAD_LOADING = 7
|
||||
const val THREAD_TYPE_BITS = 3
|
||||
const val THREAD_KEY_BITS = Long.SIZE_BITS - THREAD_TYPE_BITS
|
||||
const val THREAD_TYPE_SHIFT = THREAD_KEY_BITS
|
||||
const val THREAD_KEY_MASK = (1L shl THREAD_KEY_BITS) - 1
|
||||
|
||||
// view types for attachment list
|
||||
const val ATTACHMENT_DOCUMENT = 7
|
||||
@@ -80,7 +83,7 @@ const val FILE_SIZE_600_KB = 614_400L
|
||||
const val FILE_SIZE_1_MB = 1_048_576L
|
||||
const val FILE_SIZE_2_MB = 2_097_152L
|
||||
|
||||
const val MESSAGES_LIMIT = 30
|
||||
const val MESSAGES_LIMIT = 50
|
||||
const val MAX_MESSAGE_LENGTH = 5000
|
||||
|
||||
// intent launch request codes
|
||||
@@ -107,3 +110,8 @@ fun generateRandomId(length: Int = 9): Long {
|
||||
val random = abs(Random(millis).nextLong())
|
||||
return random.toString().takeLast(length).toLong()
|
||||
}
|
||||
|
||||
fun generateStableId(type: Int, key: Long): Long {
|
||||
require(type in 0 until (1 shl THREAD_TYPE_BITS))
|
||||
return (type.toLong() shl THREAD_TYPE_SHIFT) or (key and THREAD_KEY_MASK)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.fossify.messages.helpers
|
||||
|
||||
import android.util.LruCache
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.messages.models.NamePhoto
|
||||
|
||||
private const val CACHE_SIZE = 512
|
||||
|
||||
object MessagingCache {
|
||||
val namePhoto = LruCache<String, NamePhoto>(CACHE_SIZE)
|
||||
val participantsCache = LruCache<Long, ArrayList<SimpleContact>>(CACHE_SIZE)
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.fossify.commons.models.SimpleContact
|
||||
import org.fossify.messages.helpers.THREAD_RECEIVED_MESSAGE
|
||||
import org.fossify.messages.helpers.THREAD_SENT_MESSAGE
|
||||
import org.fossify.messages.helpers.generateStableId
|
||||
|
||||
@Entity(tableName = "messages")
|
||||
data class Message(
|
||||
@@ -34,22 +37,18 @@ data class Message(
|
||||
?: participants.firstOrNull { it.name == senderName }
|
||||
?: participants.firstOrNull()
|
||||
|
||||
fun getStableId(): Long {
|
||||
val providerBit = if (isMMS) 1L else 0L
|
||||
val key = (id shl 1) or providerBit
|
||||
val type = if (isReceivedMessage()) THREAD_RECEIVED_MESSAGE else THREAD_SENT_MESSAGE
|
||||
return generateStableId(type, key)
|
||||
}
|
||||
|
||||
fun getSelectionKey(): Int {
|
||||
return (id xor (id ushr Int.SIZE_BITS)).toInt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getStableId(message: Message): Long {
|
||||
var result = message.id.hashCode()
|
||||
result = 31 * result + message.body.hashCode()
|
||||
result = 31 * result + message.date.hashCode()
|
||||
result = 31 * result + message.threadId.hashCode()
|
||||
result = 31 * result + message.isMMS.hashCode()
|
||||
result = 31 * result + (message.attachment?.hashCode() ?: 0)
|
||||
result = 31 * result + message.senderPhoneNumber.hashCode()
|
||||
result = 31 * result + message.senderName.hashCode()
|
||||
result = 31 * result + message.senderPhotoUri.hashCode()
|
||||
result = 31 * result + message.isScheduled.hashCode()
|
||||
return result.toLong()
|
||||
}
|
||||
|
||||
fun areItemsTheSame(old: Message, new: Message): Boolean {
|
||||
return old.id == new.id
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package org.fossify.messages.models
|
||||
* Thread item representations for the main thread recyclerview. [Message] is also a [ThreadItem]
|
||||
*/
|
||||
sealed class ThreadItem {
|
||||
data class ThreadLoading(val id: Long) : ThreadItem()
|
||||
data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem()
|
||||
data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem()
|
||||
data class ThreadSent(val messageId: Long, val delivered: Boolean) : ThreadItem()
|
||||
|
||||
@@ -40,7 +40,9 @@ class DirectReplyReceiver : BroadcastReceiver() {
|
||||
var messageId = 0L
|
||||
try {
|
||||
context.sendMessageCompat(body, listOf(address), subscriptionId, emptyList())
|
||||
val message = context.getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false, limit = 1).lastOrNull()
|
||||
val message = context.getMessages(
|
||||
threadId = threadId, includeScheduledMessages = false, limit = 1
|
||||
).lastOrNull()
|
||||
if (message != null) {
|
||||
context.messagesDB.insertOrUpdate(message)
|
||||
messageId = message.id
|
||||
@@ -54,7 +56,15 @@ class DirectReplyReceiver : BroadcastReceiver() {
|
||||
val photoUri = SimpleContactsHelper(context).getPhotoUriFromPhoneNumber(address)
|
||||
val bitmap = context.getNotificationBitmap(photoUri)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
context.notificationHelper.showMessageNotification(messageId, address, body, threadId, bitmap, sender = null, alertOnlyOnce = true)
|
||||
context.notificationHelper.showMessageNotification(
|
||||
messageId = messageId,
|
||||
address = address,
|
||||
body = body,
|
||||
threadId = threadId,
|
||||
bitmap = bitmap,
|
||||
sender = null,
|
||||
alertOnlyOnce = true
|
||||
)
|
||||
}
|
||||
|
||||
context.markThreadMessagesRead(threadId)
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:paddingTop="@dimen/big_margin"
|
||||
android:paddingBottom="@dimen/medium_margin"
|
||||
android:scrollbars="none"
|
||||
app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="@dimen/normal_margin">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/thread_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="@dimen/small_margin"
|
||||
android:indeterminate="true"
|
||||
app:indicatorSize="@dimen/big_margin"
|
||||
app:trackCornerRadius="@dimen/normal_margin" />
|
||||
</LinearLayout>
|
||||
@@ -40,6 +40,11 @@ style:
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 4
|
||||
excludeGuardClauses: true
|
||||
excludes: ["**/test/**", "**/androidTest/**"]
|
||||
|
||||
naming:
|
||||
FunctionNaming:
|
||||
|
||||
Reference in New Issue
Block a user