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:
parent
d6160b8448
commit
72eb0af8ec
16 changed files with 283 additions and 251 deletions
|
|
@ -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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- Optimized loading messages in conversations
|
||||||
|
|
||||||
## [1.4.0] - 2025-10-12
|
## [1.4.0] - 2025-10-12
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,38 @@
|
||||||
package org.fossify.messages
|
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.FossifyApp
|
||||||
|
import org.fossify.commons.extensions.hasPermission
|
||||||
|
import org.fossify.commons.helpers.PERMISSION_READ_CONTACTS
|
||||||
|
import org.fossify.messages.helpers.MessagingCache
|
||||||
|
|
||||||
class App : FossifyApp() {
|
class App : FossifyApp() {
|
||||||
override val isAppLockFeatureAvailable = true
|
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) {
|
if (config.appRunCount == 1) {
|
||||||
conversations.map { it.threadId }.forEach { threadId ->
|
conversations.map { it.threadId }.forEach { threadId ->
|
||||||
val messages = getMessages(
|
val messages = getMessages(threadId, includeScheduledMessages = false)
|
||||||
threadId = threadId,
|
|
||||||
getImageResolutions = false,
|
|
||||||
includeScheduledMessages = false
|
|
||||||
)
|
|
||||||
messages.chunked(30).forEach { currentMessages ->
|
messages.chunked(30).forEach { currentMessages ->
|
||||||
messagesDB.insertMessages(*currentMessages.toTypedArray())
|
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_READ_URI_PERMISSION
|
||||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
|
|
@ -49,7 +47,6 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
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.realScreenSize
|
||||||
import org.fossify.commons.extensions.showErrorToast
|
import org.fossify.commons.extensions.showErrorToast
|
||||||
import org.fossify.commons.extensions.showKeyboard
|
import org.fossify.commons.extensions.showKeyboard
|
||||||
import org.fossify.commons.extensions.toInt
|
|
||||||
import org.fossify.commons.extensions.toast
|
import org.fossify.commons.extensions.toast
|
||||||
import org.fossify.commons.extensions.updateTextColors
|
import org.fossify.commons.extensions.updateTextColors
|
||||||
import org.fossify.commons.extensions.value
|
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.PhoneNumber
|
||||||
import org.fossify.commons.models.RadioItem
|
import org.fossify.commons.models.RadioItem
|
||||||
import org.fossify.commons.models.SimpleContact
|
import org.fossify.commons.models.SimpleContact
|
||||||
import org.fossify.commons.views.MyRecyclerView
|
|
||||||
import org.fossify.messages.BuildConfig
|
import org.fossify.messages.BuildConfig
|
||||||
import org.fossify.messages.R
|
import org.fossify.messages.R
|
||||||
import org.fossify.messages.adapters.AttachmentsAdapter
|
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.markThreadMessagesUnread
|
||||||
import org.fossify.messages.extensions.messagesDB
|
import org.fossify.messages.extensions.messagesDB
|
||||||
import org.fossify.messages.extensions.moveMessageToRecycleBin
|
import org.fossify.messages.extensions.moveMessageToRecycleBin
|
||||||
|
import org.fossify.messages.extensions.onScroll
|
||||||
import org.fossify.messages.extensions.removeDiacriticsIfNeeded
|
import org.fossify.messages.extensions.removeDiacriticsIfNeeded
|
||||||
import org.fossify.messages.extensions.renameConversation
|
import org.fossify.messages.extensions.renameConversation
|
||||||
import org.fossify.messages.extensions.restoreAllMessagesFromRecycleBinForConversation
|
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
|
||||||
import org.fossify.messages.models.ThreadItem.ThreadDateTime
|
import org.fossify.messages.models.ThreadItem.ThreadDateTime
|
||||||
import org.fossify.messages.models.ThreadItem.ThreadError
|
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.ThreadSending
|
||||||
import org.fossify.messages.models.ThreadItem.ThreadSent
|
import org.fossify.messages.models.ThreadItem.ThreadSent
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
@ -200,16 +195,11 @@ import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.fossify.messages.extensions.filterNotInByKey
|
||||||
|
|
||||||
class ThreadActivity : SimpleActivity() {
|
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 threadId = 0L
|
||||||
private var currentSIMCardIndex = 0
|
private var currentSIMCardIndex = 0
|
||||||
private var isActivityVisible = false
|
private var isActivityVisible = false
|
||||||
|
|
@ -489,12 +479,10 @@ class ThreadActivity : SimpleActivity() {
|
||||||
|
|
||||||
val cachedMessagesCode = messages.clone().hashCode()
|
val cachedMessagesCode = messages.clone().hashCode()
|
||||||
if (!isRecycleBin) {
|
if (!isRecycleBin) {
|
||||||
messages = getMessages(threadId, true)
|
messages = getMessages(threadId)
|
||||||
if (config.useRecycleBin) {
|
if (config.useRecycleBin) {
|
||||||
val recycledMessages =
|
val recycledMessages = messagesDB.getThreadMessagesFromRecycleBin(threadId)
|
||||||
messagesDB.getThreadMessagesFromRecycleBin(threadId).map { it.id }
|
messages = messages.filterNotInByKey(recycledMessages) { it.getStableId() }
|
||||||
messages = messages.filter { !recycledMessages.contains(it.id) }
|
|
||||||
.toMutableList() as ArrayList<Message>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,7 +548,6 @@ class ThreadActivity : SimpleActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAttachmentSizes()
|
|
||||||
setupAdapter()
|
setupAdapter()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
setupThreadTitle()
|
setupThreadTitle()
|
||||||
|
|
@ -587,14 +574,6 @@ class ThreadActivity : SimpleActivity() {
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.threadMessagesList.adapter = currAdapter
|
binding.threadMessagesList.adapter = currAdapter
|
||||||
binding.threadMessagesList.endlessScrollListener =
|
|
||||||
object : MyRecyclerView.EndlessScrollListener {
|
|
||||||
override fun updateBottom() {}
|
|
||||||
|
|
||||||
override fun updateTop() {
|
|
||||||
fetchNextMessages()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return currAdapter as ThreadAdapter
|
return currAdapter as ThreadAdapter
|
||||||
}
|
}
|
||||||
|
|
@ -663,21 +642,21 @@ class ThreadActivity : SimpleActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupScrollFab() {
|
private fun setupScrollListener() {
|
||||||
binding.threadMessagesList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.threadMessagesList.onScroll(
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
onScrolled = { dx, dy ->
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
tryLoadMoreMessages()
|
||||||
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
|
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
|
||||||
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
|
val lastVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||||
val isCloseToBottom =
|
val isCloseToBottom =
|
||||||
lastVisibleItemPosition >= getOrCreateThreadAdapter().itemCount - SCROLL_TO_BOTTOM_FAB_LIMIT
|
lastVisibleItemPosition >= getOrCreateThreadAdapter().itemCount - SCROLL_TO_BOTTOM_FAB_LIMIT
|
||||||
if (isCloseToBottom) {
|
val fab = binding.scrollToBottomFab
|
||||||
binding.scrollToBottomFab.hide()
|
if (isCloseToBottom) fab.hide() else fab.show()
|
||||||
} else {
|
},
|
||||||
binding.scrollToBottomFab.show()
|
onScrollStateChanged = { newState ->
|
||||||
}
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) tryLoadMoreMessages()
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleItemClick(any: Any) {
|
private fun handleItemClick(any: Any) {
|
||||||
|
|
@ -737,35 +716,25 @@ class ThreadActivity : SimpleActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchNextMessages() {
|
private fun tryLoadMoreMessages() {
|
||||||
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) {
|
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
|
||||||
if (allMessagesFetched) {
|
if (layoutManager.findFirstVisibleItemPosition() <= PREFETCH_THRESHOLD) {
|
||||||
getOrCreateThreadAdapter().apply {
|
loadMoreMessages()
|
||||||
val newList = currentList.toMutableList().apply {
|
|
||||||
removeAll { it is ThreadLoading }
|
|
||||||
}
|
|
||||||
updateMessages(
|
|
||||||
newMessages = newList as ArrayList<ThreadItem>,
|
|
||||||
scrollPosition = 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMoreMessages() {
|
||||||
|
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) return
|
||||||
|
|
||||||
val firstItem = messages.first()
|
val firstItem = messages.first()
|
||||||
val dateOfFirstItem = firstItem.date
|
val dateOfFirstItem = firstItem.date
|
||||||
if (oldestMessageDate == dateOfFirstItem) {
|
|
||||||
allMessagesFetched = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oldestMessageDate = dateOfFirstItem
|
oldestMessageDate = dateOfFirstItem
|
||||||
loadingOlderMessages = true
|
loadingOlderMessages = true
|
||||||
|
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
val olderMessages = getMessages(threadId, true, oldestMessageDate)
|
val olderMessages = getMessages(threadId, oldestMessageDate)
|
||||||
.filter { message -> !messages.contains(message) }
|
.filterNotInByKey(messages) { it.getStableId() }
|
||||||
|
|
||||||
messages.addAll(0, olderMessages)
|
messages.addAll(0, olderMessages)
|
||||||
allMessagesFetched = olderMessages.isEmpty()
|
allMessagesFetched = olderMessages.isEmpty()
|
||||||
|
|
@ -773,8 +742,7 @@ class ThreadActivity : SimpleActivity() {
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
loadingOlderMessages = false
|
loadingOlderMessages = false
|
||||||
val itemAtRefreshIndex = threadItems.indexOfFirst { it == firstItem }
|
getOrCreateThreadAdapter().updateMessages(threadItems)
|
||||||
getOrCreateThreadAdapter().updateMessages(threadItems, itemAtRefreshIndex)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -796,7 +764,7 @@ class ThreadActivity : SimpleActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupThread()
|
setupThread()
|
||||||
setupScrollFab()
|
setupScrollListener()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
|
|
@ -913,7 +881,7 @@ class ThreadActivity : SimpleActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.extras?.containsKey(THREAD_ATTACHMENT_URI) == true) {
|
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)
|
addAttachment(uri)
|
||||||
} else if (intent.extras?.containsKey(THREAD_ATTACHMENT_URIS) == true) {
|
} else if (intent.extras?.containsKey(THREAD_ATTACHMENT_URIS) == true) {
|
||||||
(intent.getSerializableExtra(THREAD_ATTACHMENT_URIS) as? ArrayList<Uri>)?.forEach {
|
(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() {
|
private fun setupParticipants() {
|
||||||
if (participants.isEmpty()) {
|
if (participants.isEmpty()) {
|
||||||
participants = if (messages.isEmpty()) {
|
participants = if (messages.isEmpty()) {
|
||||||
|
|
@ -1346,11 +1276,6 @@ class ThreadActivity : SimpleActivity() {
|
||||||
bus?.post(Events.RefreshMessages())
|
bus?.post(Events.RefreshMessages())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allMessagesFetched && messages.size >= MESSAGES_LIMIT) {
|
|
||||||
val threadLoading = ThreadLoading(generateRandomId())
|
|
||||||
items.add(0, threadLoading)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1615,13 +1540,8 @@ class ThreadActivity : SimpleActivity() {
|
||||||
refreshedSinceSent = false
|
refreshedSinceSent = false
|
||||||
sendMessageCompat(text, addresses, subscriptionId, attachments, messageToResend)
|
sendMessageCompat(text, addresses, subscriptionId, attachments, messageToResend)
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
val messageIds = messages.map { it.id }
|
val messages = getMessages(threadId, limit = maxOf(1, attachments.size))
|
||||||
val messages = getMessages(
|
.filterNotInByKey(messages) { it.getStableId() }
|
||||||
threadId = threadId,
|
|
||||||
getImageResolutions = true,
|
|
||||||
limit = maxOf(1, attachments.size)
|
|
||||||
)
|
|
||||||
.filter { it.id !in messageIds }
|
|
||||||
for (message in messages) {
|
for (message in messages) {
|
||||||
insertOrUpdateMessage(message)
|
insertOrUpdateMessage(message)
|
||||||
}
|
}
|
||||||
|
|
@ -1805,9 +1725,7 @@ class ThreadActivity : SimpleActivity() {
|
||||||
|
|
||||||
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
|
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
|
||||||
val newThreadId = getThreadId(participants.getAddresses().toSet())
|
val newThreadId = getThreadId(participants.getAddresses().toSet())
|
||||||
val newMessages =
|
val newMessages = getMessages(newThreadId, includeScheduledMessages = false)
|
||||||
getMessages(newThreadId, getImageResolutions = true, includeScheduledMessages = false)
|
|
||||||
|
|
||||||
if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
|
if (messages.isNotEmpty() && messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
|
||||||
// update scheduled messages with real thread id
|
// update scheduled messages with real thread id
|
||||||
threadId = newThreadId
|
threadId = newThreadId
|
||||||
|
|
@ -2145,4 +2063,13 @@ class ThreadActivity : SimpleActivity() {
|
||||||
} else {
|
} else {
|
||||||
getBottomNavigationBackgroundColor()
|
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.Color
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.util.Size
|
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -14,21 +13,34 @@ import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
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.load.resource.bitmap.FitCenter
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
||||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
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.SimpleContactsHelper
|
||||||
import org.fossify.commons.helpers.ensureBackgroundThread
|
import org.fossify.commons.helpers.ensureBackgroundThread
|
||||||
import org.fossify.commons.views.MyRecyclerView
|
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.SimpleActivity
|
||||||
import org.fossify.messages.activities.ThreadActivity
|
import org.fossify.messages.activities.ThreadActivity
|
||||||
import org.fossify.messages.activities.VCardViewerActivity
|
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.DeleteConfirmationDialog
|
||||||
import org.fossify.messages.dialogs.MessageDetailsDialog
|
import org.fossify.messages.dialogs.MessageDetailsDialog
|
||||||
import org.fossify.messages.dialogs.SelectTextDialog
|
import org.fossify.messages.dialogs.SelectTextDialog
|
||||||
import org.fossify.messages.extensions.*
|
import org.fossify.messages.extensions.config
|
||||||
import org.fossify.messages.helpers.*
|
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.Attachment
|
||||||
import org.fossify.messages.models.Message
|
import org.fossify.messages.models.Message
|
||||||
import org.fossify.messages.models.ThreadItem
|
import org.fossify.messages.models.ThreadItem
|
||||||
import org.fossify.messages.models.ThreadItem.*
|
import org.fossify.messages.models.ThreadItem.ThreadDateTime
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import org.fossify.messages.models.ThreadItem.ThreadError
|
||||||
|
import org.fossify.messages.models.ThreadItem.ThreadSending
|
||||||
|
import org.fossify.messages.models.ThreadItem.ThreadSent
|
||||||
|
|
||||||
class ThreadAdapter(
|
class ThreadAdapter(
|
||||||
activity: SimpleActivity,
|
activity: SimpleActivity,
|
||||||
|
|
@ -60,11 +97,18 @@ class ThreadAdapter(
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private val hasMultipleSIMCards = (activity.subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1
|
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 {
|
init {
|
||||||
setupDragListener(true)
|
setupDragListener(true)
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
|
(recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActionMenuId() = R.menu.cab_thread
|
override fun getActionMenuId() = R.menu.cab_thread
|
||||||
|
|
@ -110,9 +154,13 @@ class ThreadAdapter(
|
||||||
|
|
||||||
override fun getIsItemSelectable(position: Int) = !isThreadDateTime(position)
|
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() {}
|
override fun onActionModeCreated() {}
|
||||||
|
|
||||||
|
|
@ -120,7 +168,6 @@ class ThreadAdapter(
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val binding = when (viewType) {
|
val binding = when (viewType) {
|
||||||
THREAD_LOADING -> ItemThreadLoadingBinding.inflate(layoutInflater, parent, false)
|
|
||||||
THREAD_DATE_TIME -> ItemThreadDateTimeBinding.inflate(layoutInflater, parent, false)
|
THREAD_DATE_TIME -> ItemThreadDateTimeBinding.inflate(layoutInflater, parent, false)
|
||||||
THREAD_SENT_MESSAGE_ERROR -> ItemThreadErrorBinding.inflate(layoutInflater, parent, false)
|
THREAD_SENT_MESSAGE_ERROR -> ItemThreadErrorBinding.inflate(layoutInflater, parent, false)
|
||||||
THREAD_SENT_MESSAGE_SENT -> ItemThreadSuccessBinding.inflate(layoutInflater, parent, false)
|
THREAD_SENT_MESSAGE_SENT -> ItemThreadSuccessBinding.inflate(layoutInflater, parent, false)
|
||||||
|
|
@ -137,7 +184,6 @@ class ThreadAdapter(
|
||||||
val isLongClickable = item is Message
|
val isLongClickable = item is Message
|
||||||
holder.bindView(item, isClickable, isLongClickable) { itemView, _ ->
|
holder.bindView(item, isClickable, isLongClickable) { itemView, _ ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is ThreadLoading -> setupThreadLoading(itemView)
|
|
||||||
is ThreadDateTime -> setupDateTime(itemView, item)
|
is ThreadDateTime -> setupDateTime(itemView, item)
|
||||||
is ThreadError -> setupThreadError(itemView)
|
is ThreadError -> setupThreadError(itemView)
|
||||||
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
|
is ThreadSent -> setupThreadSuccess(itemView, item.delivered)
|
||||||
|
|
@ -150,14 +196,20 @@ class ThreadAdapter(
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
return when (val item = getItem(position)) {
|
return when (val item = getItem(position)) {
|
||||||
is Message -> Message.getStableId(item)
|
is Message -> item.getStableId()
|
||||||
else -> item.hashCode().toLong()
|
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 {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return when (val item = getItem(position)) {
|
return when (val item = getItem(position)) {
|
||||||
is ThreadLoading -> THREAD_LOADING
|
|
||||||
is ThreadDateTime -> THREAD_DATE_TIME
|
is ThreadDateTime -> THREAD_DATE_TIME
|
||||||
is ThreadError -> THREAD_SENT_MESSAGE_ERROR
|
is ThreadError -> THREAD_SENT_MESSAGE_ERROR
|
||||||
is ThreadSent -> THREAD_SENT_MESSAGE_SENT
|
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
|
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) {
|
private fun setupView(holder: ViewHolder, view: View, message: Message) {
|
||||||
ItemMessageBinding.bind(view).apply {
|
ItemMessageBinding.bind(view).apply {
|
||||||
threadMessageHolder.isSelected = selectedKeys.contains(message.hashCode())
|
threadMessageHolder.isSelected = selectedKeys.contains(message.getSelectionKey())
|
||||||
threadMessageBody.apply {
|
threadMessageBody.apply {
|
||||||
text = message.body
|
text = message.body
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
|
||||||
|
|
@ -416,16 +472,17 @@ class ThreadAdapter(
|
||||||
threadMessageAttachmentsHolder.addView(imageView.root)
|
threadMessageAttachmentsHolder.addView(imageView.root)
|
||||||
|
|
||||||
val placeholderDrawable = Color.TRANSPARENT.toDrawable()
|
val placeholderDrawable = Color.TRANSPARENT.toDrawable()
|
||||||
val isTallImage = attachment.height > attachment.width
|
|
||||||
val transformation = if (isTallImage) CenterCrop() else FitCenter()
|
|
||||||
val options = RequestOptions()
|
val options = RequestOptions()
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
.placeholder(placeholderDrawable)
|
.placeholder(placeholderDrawable)
|
||||||
.transform(transformation)
|
.transform(FitCenter())
|
||||||
|
|
||||||
var builder = Glide.with(root.context)
|
Glide.with(root.context)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.apply(options)
|
.apply(options)
|
||||||
|
.dontAnimate()
|
||||||
|
.override(maxChatBubbleWidth, maxChatBubbleWidth * MAX_MEDIA_HEIGHT_RATIO)
|
||||||
|
.downsample(DownsampleStrategy.AT_MOST)
|
||||||
.listener(object : RequestListener<Drawable> {
|
.listener(object : RequestListener<Drawable> {
|
||||||
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>, isFirstResource: Boolean): Boolean {
|
||||||
threadMessagePlayOutline.beGone()
|
threadMessagePlayOutline.beGone()
|
||||||
|
|
@ -435,23 +492,11 @@ class ThreadAdapter(
|
||||||
|
|
||||||
override fun onResourceReady(dr: Drawable, a: Any, t: Target<Drawable>, d: DataSource, i: Boolean) = false
|
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
|
imageView.attachmentImage.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||||
var wantedAttachmentSize = Size(attachment.width, attachment.height)
|
width = maxChatBubbleWidth
|
||||||
if (wantedAttachmentSize.width > maxChatBubbleWidth) {
|
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
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.setOnClickListener {
|
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) {
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
if (!activity.isDestroyed && !activity.isFinishing) {
|
if (!activity.isDestroyed && !activity.isFinishing) {
|
||||||
|
|
@ -576,19 +616,21 @@ private class ThreadItemDiffCallback : DiffUtil.ItemCallback<ThreadItem>() {
|
||||||
override fun areItemsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean {
|
override fun areItemsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean {
|
||||||
if (oldItem::class.java != newItem::class.java) return false
|
if (oldItem::class.java != newItem::class.java) return false
|
||||||
return when (oldItem) {
|
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 ThreadError -> oldItem.messageId == (newItem as ThreadError).messageId
|
||||||
is ThreadSent -> oldItem.messageId == (newItem as ThreadSent).messageId
|
is ThreadSent -> oldItem.messageId == (newItem as ThreadSent).messageId
|
||||||
is ThreadSending -> oldItem.messageId == (newItem as ThreadSending).messageId
|
is ThreadSending -> oldItem.messageId == (newItem as ThreadSending).messageId
|
||||||
is Message -> Message.areItemsTheSame(oldItem, newItem as Message)
|
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 {
|
override fun areContentsTheSame(oldItem: ThreadItem, newItem: ThreadItem): Boolean {
|
||||||
if (oldItem::class.java != newItem::class.java) return false
|
if (oldItem::class.java != newItem::class.java) return false
|
||||||
return when (oldItem) {
|
return when (oldItem) {
|
||||||
is ThreadLoading, is ThreadSending -> true
|
is ThreadSending -> true
|
||||||
is ThreadDateTime -> oldItem.simID == (newItem as ThreadDateTime).simID
|
is ThreadDateTime -> oldItem.simID == (newItem as ThreadDateTime).simID
|
||||||
is ThreadError -> oldItem.messageText == (newItem as ThreadError).messageText
|
is ThreadError -> oldItem.messageText == (newItem as ThreadError).messageText
|
||||||
is ThreadSent -> oldItem.delivered == (newItem as ThreadSent).delivered
|
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)
|
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.Cursor
|
||||||
import android.database.sqlite.SQLiteException
|
import android.database.sqlite.SQLiteException
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
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.FILE_SIZE_NONE
|
||||||
import org.fossify.messages.helpers.MAX_MESSAGE_LENGTH
|
import org.fossify.messages.helpers.MAX_MESSAGE_LENGTH
|
||||||
import org.fossify.messages.helpers.MESSAGES_LIMIT
|
import org.fossify.messages.helpers.MESSAGES_LIMIT
|
||||||
|
import org.fossify.messages.helpers.MessagingCache
|
||||||
import org.fossify.messages.helpers.NotificationHelper
|
import org.fossify.messages.helpers.NotificationHelper
|
||||||
import org.fossify.messages.helpers.ShortcutHelper
|
import org.fossify.messages.helpers.ShortcutHelper
|
||||||
import org.fossify.messages.helpers.generateRandomId
|
import org.fossify.messages.helpers.generateRandomId
|
||||||
|
|
@ -111,7 +111,6 @@ val Context.shortcutHelper get() = ShortcutHelper(this)
|
||||||
|
|
||||||
fun Context.getMessages(
|
fun Context.getMessages(
|
||||||
threadId: Long,
|
threadId: Long,
|
||||||
getImageResolutions: Boolean,
|
|
||||||
dateFrom: Int = -1,
|
dateFrom: Int = -1,
|
||||||
includeScheduledMessages: Boolean = true,
|
includeScheduledMessages: Boolean = true,
|
||||||
limit: Int = MESSAGES_LIMIT,
|
limit: Int = MESSAGES_LIMIT,
|
||||||
|
|
@ -139,15 +138,7 @@ fun Context.getMessages(
|
||||||
var messages = ArrayList<Message>()
|
var messages = ArrayList<Message>()
|
||||||
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
||||||
val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor
|
val senderNumber = cursor.getStringValue(Sms.ADDRESS) ?: return@queryCursor
|
||||||
|
val isNumberBlocked = blockStatus.getOrPut(senderNumber) { isNumberBlocked(senderNumber, blockedNumbers) }
|
||||||
val isNumberBlocked = if (blockStatus.containsKey(senderNumber)) {
|
|
||||||
blockStatus[senderNumber]!!
|
|
||||||
} else {
|
|
||||||
val isBlocked = isNumberBlocked(senderNumber, blockedNumbers)
|
|
||||||
blockStatus[senderNumber] = isBlocked
|
|
||||||
isBlocked
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNumberBlocked) {
|
if (isNumberBlocked) {
|
||||||
return@queryCursor
|
return@queryCursor
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +192,7 @@ fun Context.getMessages(
|
||||||
messages.add(message)
|
messages.add(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.addAll(getMMS(threadId, getImageResolutions, sortOrder, dateFrom))
|
messages.addAll(getMMS(threadId, sortOrder, dateFrom))
|
||||||
|
|
||||||
if (includeScheduledMessages) {
|
if (includeScheduledMessages) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -225,7 +216,6 @@ fun Context.getMessages(
|
||||||
// as soon as a message contains multiple recipients it counts as an MMS instead of SMS
|
// as soon as a message contains multiple recipients it counts as an MMS instead of SMS
|
||||||
fun Context.getMMS(
|
fun Context.getMMS(
|
||||||
threadId: Long? = null,
|
threadId: Long? = null,
|
||||||
getImageResolutions: Boolean = false,
|
|
||||||
sortOrder: String? = null,
|
sortOrder: String? = null,
|
||||||
dateFrom: Int = -1,
|
dateFrom: Int = -1,
|
||||||
): ArrayList<Message> {
|
): ArrayList<Message> {
|
||||||
|
|
@ -256,7 +246,6 @@ fun Context.getMMS(
|
||||||
|
|
||||||
val messages = ArrayList<Message>()
|
val messages = ArrayList<Message>()
|
||||||
val contactsMap = HashMap<Int, SimpleContact>()
|
val contactsMap = HashMap<Int, SimpleContact>()
|
||||||
val threadParticipants = HashMap<Long, ArrayList<SimpleContact>>()
|
|
||||||
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
queryCursor(uri, projection, selection, selectionArgs, sortOrder, showErrors = true) { cursor ->
|
||||||
val mmsId = cursor.getLongValue(Mms._ID)
|
val mmsId = cursor.getLongValue(Mms._ID)
|
||||||
val type = cursor.getIntValue(Mms.MESSAGE_BOX)
|
val type = cursor.getIntValue(Mms.MESSAGE_BOX)
|
||||||
|
|
@ -265,16 +254,10 @@ fun Context.getMMS(
|
||||||
val threadId = cursor.getLongValue(Mms.THREAD_ID)
|
val threadId = cursor.getLongValue(Mms.THREAD_ID)
|
||||||
val subscriptionId = cursor.getIntValue(Mms.SUBSCRIPTION_ID)
|
val subscriptionId = cursor.getIntValue(Mms.SUBSCRIPTION_ID)
|
||||||
val status = cursor.getIntValue(Mms.STATUS)
|
val status = cursor.getIntValue(Mms.STATUS)
|
||||||
val participants = if (threadParticipants.containsKey(threadId)) {
|
val participants = getThreadParticipants(threadId, contactsMap)
|
||||||
threadParticipants[threadId]!!
|
|
||||||
} else {
|
|
||||||
val parts = getThreadParticipants(threadId, contactsMap)
|
|
||||||
threadParticipants[threadId] = parts
|
|
||||||
parts
|
|
||||||
}
|
|
||||||
|
|
||||||
val isMMS = true
|
val isMMS = true
|
||||||
val attachment = getMmsAttachment(mmsId, getImageResolutions)
|
val attachment = getMmsAttachment(mmsId)
|
||||||
val body = attachment.text
|
val body = attachment.text
|
||||||
var senderNumber = ""
|
var senderNumber = ""
|
||||||
var senderName = ""
|
var senderName = ""
|
||||||
|
|
@ -478,7 +461,7 @@ fun Context.getConversationIds(): List<Long> {
|
||||||
|
|
||||||
// based on https://stackoverflow.com/a/6446831/1967672
|
// based on https://stackoverflow.com/a/6446831/1967672
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAttachment {
|
fun Context.getMmsAttachment(id: Long): MessageAttachment {
|
||||||
val uri = if (isQPlus()) {
|
val uri = if (isQPlus()) {
|
||||||
Mms.Part.CONTENT_URI
|
Mms.Part.CONTENT_URI
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -506,32 +489,14 @@ fun Context.getMmsAttachment(id: Long, getImageResolutions: Boolean): MessageAtt
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
} else if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
|
} else if (mimetype.startsWith("image/") || mimetype.startsWith("video/")) {
|
||||||
val fileUri = Uri.withAppendedPath(uri, partId.toString())
|
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(
|
messageAttachment.attachments.add(
|
||||||
Attachment(
|
Attachment(
|
||||||
id = partId,
|
id = partId,
|
||||||
messageId = id,
|
messageId = id,
|
||||||
uriString = fileUri.toString(),
|
uriString = fileUri.toString(),
|
||||||
mimetype = mimetype,
|
mimetype = mimetype,
|
||||||
width = width,
|
width = 0,
|
||||||
height = height,
|
height = 0,
|
||||||
filename = ""
|
filename = ""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -569,7 +534,7 @@ fun Context.getLatestMMS(): Message? {
|
||||||
|
|
||||||
fun Context.getThreadSnippet(threadId: Long): String {
|
fun Context.getThreadSnippet(threadId: Long): String {
|
||||||
val sortOrder = "${Mms.DATE} DESC LIMIT 1"
|
val sortOrder = "${Mms.DATE} DESC LIMIT 1"
|
||||||
val latestMms = getMMS(threadId, false, sortOrder).firstOrNull()
|
val latestMms = getMMS(threadId, sortOrder).firstOrNull()
|
||||||
var snippet = latestMms?.body ?: ""
|
var snippet = latestMms?.body ?: ""
|
||||||
|
|
||||||
val uri = Sms.CONTENT_URI
|
val uri = Sms.CONTENT_URI
|
||||||
|
|
@ -620,6 +585,16 @@ fun Context.getThreadParticipants(
|
||||||
threadId: Long,
|
threadId: Long,
|
||||||
contactsMap: HashMap<Int, SimpleContact>?,
|
contactsMap: HashMap<Int, SimpleContact>?,
|
||||||
): ArrayList<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 uri = "${MmsSms.CONTENT_CONVERSATIONS_URI}?simple=true".toUri()
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
ThreadsColumns.RECIPIENT_IDS
|
ThreadsColumns.RECIPIENT_IDS
|
||||||
|
|
@ -660,6 +635,8 @@ fun Context.getThreadParticipants(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showErrorToast(e)
|
showErrorToast(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessagingCache.participantsCache.put(threadId, participants)
|
||||||
return participants
|
return participants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -768,6 +745,7 @@ fun Context.getSuggestedContacts(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
|
fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
|
||||||
|
MessagingCache.namePhoto.get(number)?.let { return it }
|
||||||
if (!hasPermission(PERMISSION_READ_CONTACTS)) {
|
if (!hasPermission(PERMISSION_READ_CONTACTS)) {
|
||||||
return NamePhoto(number, null)
|
return NamePhoto(number, null)
|
||||||
}
|
}
|
||||||
|
|
@ -778,19 +756,23 @@ fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto {
|
||||||
PhoneLookup.PHOTO_URI
|
PhoneLookup.PHOTO_URI
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
val result = try {
|
||||||
val cursor = contentResolver.query(uri, projection, null, null, null)
|
val cursor = contentResolver.query(uri, projection, null, null, null)
|
||||||
cursor.use {
|
cursor.use {
|
||||||
if (cursor?.moveToFirst() == true) {
|
if (cursor?.moveToFirst() == true) {
|
||||||
val name = cursor.getStringValue(PhoneLookup.DISPLAY_NAME)
|
val name = cursor.getStringValue(PhoneLookup.DISPLAY_NAME)
|
||||||
val photoUri = cursor.getStringValue(PhoneLookup.PHOTO_URI)
|
val photoUri = cursor.getStringValue(PhoneLookup.PHOTO_URI)
|
||||||
return NamePhoto(name, photoUri)
|
NamePhoto(name, photoUri)
|
||||||
|
} else {
|
||||||
|
NamePhoto(number, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
NamePhoto(number, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NamePhoto(number, null)
|
MessagingCache.namePhoto.put(number, result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.insertNewSMS(
|
fun Context.insertNewSMS(
|
||||||
|
|
@ -856,6 +838,7 @@ fun Context.deleteConversation(threadId: Long) {
|
||||||
|
|
||||||
conversationsDB.deleteThreadId(threadId)
|
conversationsDB.deleteThreadId(threadId)
|
||||||
messagesDB.deleteThreadMessages(threadId)
|
messagesDB.deleteThreadMessages(threadId)
|
||||||
|
MessagingCache.participantsCache.remove(threadId)
|
||||||
|
|
||||||
if (config.customNotifications.contains(threadId.toString())) {
|
if (config.customNotifications.contains(threadId.toString())) {
|
||||||
config.removeCustomNotificationsByThreadId(threadId)
|
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_ERROR = 4
|
||||||
const val THREAD_SENT_MESSAGE_SENT = 5
|
const val THREAD_SENT_MESSAGE_SENT = 5
|
||||||
const val THREAD_SENT_MESSAGE_SENDING = 6
|
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
|
// view types for attachment list
|
||||||
const val ATTACHMENT_DOCUMENT = 7
|
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_1_MB = 1_048_576L
|
||||||
const val FILE_SIZE_2_MB = 2_097_152L
|
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
|
const val MAX_MESSAGE_LENGTH = 5000
|
||||||
|
|
||||||
// intent launch request codes
|
// intent launch request codes
|
||||||
|
|
@ -107,3 +110,8 @@ fun generateRandomId(length: Int = 9): Long {
|
||||||
val random = abs(Random(millis).nextLong())
|
val random = abs(Random(millis).nextLong())
|
||||||
return random.toString().takeLast(length).toLong()
|
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.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import org.fossify.commons.models.SimpleContact
|
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")
|
@Entity(tableName = "messages")
|
||||||
data class Message(
|
data class Message(
|
||||||
|
|
@ -34,22 +37,18 @@ data class Message(
|
||||||
?: participants.firstOrNull { it.name == senderName }
|
?: participants.firstOrNull { it.name == senderName }
|
||||||
?: participants.firstOrNull()
|
?: 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 {
|
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 {
|
fun areItemsTheSame(old: Message, new: Message): Boolean {
|
||||||
return old.id == new.id
|
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]
|
* Thread item representations for the main thread recyclerview. [Message] is also a [ThreadItem]
|
||||||
*/
|
*/
|
||||||
sealed class ThreadItem {
|
sealed class ThreadItem {
|
||||||
data class ThreadLoading(val id: Long) : ThreadItem()
|
|
||||||
data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem()
|
data class ThreadDateTime(val date: Int, val simID: String) : ThreadItem()
|
||||||
data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem()
|
data class ThreadError(val messageId: Long, val messageText: String) : ThreadItem()
|
||||||
data class ThreadSent(val messageId: Long, val delivered: Boolean) : ThreadItem()
|
data class ThreadSent(val messageId: Long, val delivered: Boolean) : ThreadItem()
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ class DirectReplyReceiver : BroadcastReceiver() {
|
||||||
var messageId = 0L
|
var messageId = 0L
|
||||||
try {
|
try {
|
||||||
context.sendMessageCompat(body, listOf(address), subscriptionId, emptyList())
|
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) {
|
if (message != null) {
|
||||||
context.messagesDB.insertOrUpdate(message)
|
context.messagesDB.insertOrUpdate(message)
|
||||||
messageId = message.id
|
messageId = message.id
|
||||||
|
|
@ -54,7 +56,15 @@ class DirectReplyReceiver : BroadcastReceiver() {
|
||||||
val photoUri = SimpleContactsHelper(context).getPhotoUriFromPhoneNumber(address)
|
val photoUri = SimpleContactsHelper(context).getPhotoUriFromPhoneNumber(address)
|
||||||
val bitmap = context.getNotificationBitmap(photoUri)
|
val bitmap = context.getNotificationBitmap(photoUri)
|
||||||
Handler(Looper.getMainLooper()).post {
|
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)
|
context.markThreadMessagesRead(threadId)
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:overScrollMode="ifContentScrolls"
|
android:overScrollMode="ifContentScrolls"
|
||||||
|
android:paddingTop="@dimen/big_margin"
|
||||||
android:paddingBottom="@dimen/medium_margin"
|
android:paddingBottom="@dimen/medium_margin"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
app:layoutManager="org.fossify.commons.views.MyLinearLayoutManager"
|
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
|
maxLineLength: 120
|
||||||
excludePackageStatements: true
|
excludePackageStatements: true
|
||||||
excludeImportStatements: true
|
excludeImportStatements: true
|
||||||
|
ReturnCount:
|
||||||
|
active: true
|
||||||
|
max: 4
|
||||||
|
excludeGuardClauses: true
|
||||||
|
excludes: ["**/test/**", "**/androidTest/**"]
|
||||||
|
|
||||||
naming:
|
naming:
|
||||||
FunctionNaming:
|
FunctionNaming:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue