Handle conversations with scheduled messages only

This commit is contained in:
Naveen 2022-09-28 02:05:06 +05:30
parent f837790948
commit ee8130c767
11 changed files with 171 additions and 36 deletions

View File

@ -208,29 +208,51 @@ class MainActivity : SimpleActivity() {
setupConversations(conversations)
getNewConversations(conversations)
}
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
}
}
}
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
val privateCursor = getMyContactsCursor(false, true)
val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread {
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val conversations = getConversations(privateContacts = privateContacts)
val scheduledConversations = cachedConversations.filter { it.isScheduled }
val allConversations = conversations.toArrayList().apply {
addAll(scheduledConversations)
}
runOnUiThread {
setupConversations(conversations)
setupConversations(allConversations)
}
conversations.forEach { clonedConversation ->
if (!cachedConversations.map { it.threadId }.contains(clonedConversation.threadId)) {
val threadIds = cachedConversations.map { it.threadId }
if (!threadIds.contains(clonedConversation.threadId)) {
conversationsDB.insertOrUpdate(clonedConversation)
cachedConversations.add(clonedConversation)
}
}
cachedConversations.forEach { cachedConversation ->
if (!conversations.map { it.threadId }.contains(cachedConversation.threadId)) {
conversationsDB.deleteThreadId(cachedConversation.threadId)
val threadId = cachedConversation.threadId
val isTemporaryThread = cachedConversation.isScheduled
val isConversationDeleted = !conversations.map { it.threadId }.contains(threadId)
if (isConversationDeleted && !isTemporaryThread) {
conversationsDB.deleteThreadId(threadId)
}
val newConversation = conversations.find { it.phoneNumber == cachedConversation.phoneNumber }
if (isTemporaryThread && newConversation != null) {
// delete the original temporary thread and move any scheduled messages to the new thread
conversationsDB.deleteThreadId(threadId)
messagesDB.getScheduledThreadMessages(threadId)
.forEach { message ->
messagesDB.insertOrUpdate(message.copy(threadId = newConversation.threadId))
}
}
}
@ -273,8 +295,9 @@ class MainActivity : SimpleActivity() {
hideKeyboard()
ConversationsAdapter(this, sortedConversations, conversations_list) {
Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, (it as Conversation).threadId)
putExtra(THREAD_TITLE, it.title)
val conversation = it as Conversation
putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
startActivity(this)
}
}.apply {

View File

@ -232,6 +232,8 @@ class ThreadActivity : SimpleActivity() {
} catch (e: Exception) {
ArrayList()
}
clearExpiredScheduledMessages(threadId, messages)
messages.removeAll { it.isScheduled && it.millis() < System.currentTimeMillis() }
messages.sortBy { it.date }
if (messages.size > MESSAGES_LIMIT) {
@ -330,9 +332,13 @@ class ThreadActivity : SimpleActivity() {
val currAdapter = thread_messages_list.adapter
if (currAdapter == null) {
ThreadAdapter(this, threadItems, thread_messages_list) { any ->
handleItemClick(any)
}.apply {
ThreadAdapter(
activity = this,
messages = threadItems,
recyclerView = thread_messages_list,
itemClick = { handleItemClick(it) },
onThreadIdUpdate = { threadId = it }
).apply {
thread_messages_list.adapter = this
}
@ -953,8 +959,13 @@ class ThreadActivity : SimpleActivity() {
refreshedSinceSent = false
try {
ensureBackgroundThread {
val messageId = scheduledMessage?.id ?: generateRandomMessageId()
val messageId = scheduledMessage?.id ?: generateRandomId()
val message = buildScheduledMessage(text, subscriptionId, messageId)
if (messages.isEmpty()) {
// create a temporary thread until a real message is sent
threadId = message.threadId
createTemporaryThread(message, message.threadId)
}
messagesDB.insertOrUpdate(message)
scheduleMessage(message)
}
@ -1140,8 +1151,18 @@ class ThreadActivity : SimpleActivity() {
notificationManager.cancel(threadId.hashCode())
}
val lastMaxId = messages.maxByOrNull { it.id }?.id ?: 0L
messages = getMessages(threadId, true)
val newThreadId = getThreadId(participants.getAddresses().toSet())
val newMessages = getMessages(newThreadId, false)
messages = if (messages.all { it.isScheduled } && newMessages.isNotEmpty()) {
threadId = newThreadId
// update scheduled messages with real thread id
updateScheduledMessagesThreadId(messages, newThreadId)
getMessages(newThreadId, true)
} else {
getMessages(threadId, true)
}
val lastMaxId = messages.filterNot { it.isScheduled }.maxByOrNull { it.id }?.id ?: 0L
messages.filter { !it.isReceivedMessage() && it.id > lastMaxId }.forEach { latestMessage ->
// subscriptionIds seem to be not filled out at sending with multiple SIM cards, so fill it manually
@ -1233,7 +1254,7 @@ class ThreadActivity : SimpleActivity() {
hideScheduleSendUi()
if (scheduledMessage != null) {
ensureBackgroundThread {
messagesDB.delete(scheduledMessage!!.id)
deleteScheduledMessage(scheduledMessage!!.id)
refreshMessages()
}
}
@ -1267,6 +1288,7 @@ class ThreadActivity : SimpleActivity() {
}
private fun buildScheduledMessage(text: String, subscriptionId: Int, messageId: Long): Message {
val threadId = if (messages.isEmpty()) messageId else threadId
return Message(
id = messageId,
body = text,
@ -1287,8 +1309,9 @@ class ThreadActivity : SimpleActivity() {
private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment {
val attachments = attachmentSelections.values
.map { Attachment(null, messageId, it.uri.toString(), "*/*", 0, 0, "") }
.map { Attachment(null, messageId, it.uri.toString(), contentResolver.getType(it.uri) ?: "*/*", 0, 0, "") }
.toArrayList()
return MessageAttachment(messageId, text, attachments)
}
}

View File

@ -173,7 +173,7 @@ class ConversationsAdapter(
}
try {
conversations.removeAll(conversationsToRemove)
conversations.removeAll(conversationsToRemove.toSet())
} catch (ignored: Exception) {
}
@ -319,15 +319,16 @@ class ConversationsAdapter(
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
}
if (conversation.read) {
conversation_address.setTypeface(null, Typeface.NORMAL)
conversation_body_short.setTypeface(null, Typeface.NORMAL)
val style = if (conversation.read) {
conversation_body_short.alpha = 0.7f
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
} else {
conversation_address.setTypeface(null, Typeface.BOLD)
conversation_body_short.setTypeface(null, Typeface.BOLD)
conversation_body_short.alpha = 1f
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
}
conversation_address.setTypeface(null, style)
conversation_body_short.setTypeface(null, style)
arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
it.setTextColor(textColor)

View File

@ -51,9 +51,10 @@ import kotlinx.android.synthetic.main.item_thread_date_time.view.*
import kotlinx.android.synthetic.main.item_thread_error.view.*
import kotlinx.android.synthetic.main.item_thread_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.*
import java.util.*
class ThreadAdapter(
activity: SimpleActivity, var messages: ArrayList<ThreadItem>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit
activity: SimpleActivity, var messages: ArrayList<ThreadItem>, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, val onThreadIdUpdate: (Long) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
private var fontSize = activity.getTextSize()
@ -204,11 +205,21 @@ class ThreadAdapter(
messagesToRemove.forEach {
activity.deleteMessage((it as Message).id, it.isMMS)
}
messages.removeAll(messagesToRemove)
messages.removeAll(messagesToRemove.toSet())
activity.updateLastConversationMessage(threadId)
val messages = messages.filterIsInstance<Message>()
if (messages.isNotEmpty() && messages.all { it.isScheduled }) {
// move all scheduled messages to a temporary thread as there are no real messages left
val message = messages.last()
val newThreadId = generateRandomId()
activity.createTemporaryThread(message, newThreadId)
activity.updateScheduledMessagesThreadId(messages, newThreadId)
onThreadIdUpdate(newThreadId)
}
activity.runOnUiThread {
if (messages.filter { it is Message }.isEmpty()) {
if (messages.isEmpty()) {
activity.finish()
} else {
removeSelectedItems(positions)
@ -333,7 +344,7 @@ class ThreadAdapter(
thread_message_scheduled_icon.beGone()
thread_message_body.setPadding(padding, padding, padding, padding)
thread_message_body.typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
thread_message_body.typeface = Typeface.DEFAULT
}
}
}
@ -495,7 +506,7 @@ class ThreadAdapter(
private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) {
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(uri, mimetype.toLowerCase())
setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {

View File

@ -67,8 +67,10 @@ abstract class MessagesDatabase : RoomDatabase() {
database.apply {
execSQL("CREATE TABLE conversations_new (`thread_id` INTEGER NOT NULL PRIMARY KEY, `snippet` TEXT NOT NULL, `date` INTEGER NOT NULL, `read` INTEGER NOT NULL, `title` TEXT NOT NULL, `photo_uri` TEXT NOT NULL, `is_group_conversation` INTEGER NOT NULL, `phone_number` TEXT NOT NULL)")
execSQL("INSERT OR IGNORE INTO conversations_new (thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number) " +
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations")
execSQL(
"INSERT OR IGNORE INTO conversations_new (thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number) " +
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations"
)
execSQL("DROP TABLE conversations")
@ -91,6 +93,7 @@ abstract class MessagesDatabase : RoomDatabase() {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("ALTER TABLE messages ADD COLUMN is_scheduled INTEGER NOT NULL DEFAULT 0")
execSQL("ALTER TABLE conversations ADD COLUMN is_scheduled INTEGER NOT NULL DEFAULT 0")
}
}
}

View File

@ -118,8 +118,12 @@ fun Context.getMessages(threadId: Long, getImageResolutions: Boolean, dateFrom:
messages.addAll(getMMS(threadId, getImageResolutions, sortOrder))
if (includeScheduledMessages) {
val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId)
messages.addAll(scheduledMessages)
try {
val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId)
messages.addAll(scheduledMessages)
} catch (e: Exception) {
e.printStackTrace()
}
}
messages = messages
@ -580,7 +584,11 @@ fun Context.deleteConversation(threadId: Long) {
}
uri = Mms.CONTENT_URI
contentResolver.delete(uri, selection, selectionArgs)
try {
contentResolver.delete(uri, selection, selectionArgs)
} catch (e: Exception) {
e.printStackTrace()
}
conversationsDB.deleteThreadId(threadId)
messagesDB.deleteThreadMessages(threadId)
@ -598,6 +606,14 @@ fun Context.deleteMessage(id: Long, isMMS: Boolean) {
}
}
fun Context.deleteScheduledMessage(messageId: Long) {
try {
messagesDB.delete(messageId)
} catch (e: Exception) {
showErrorToast(e)
}
}
fun Context.markMessageRead(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val contentValues = ContentValues().apply {
@ -1001,3 +1017,51 @@ fun Context.subscriptionManagerCompat(): SubscriptionManager {
SubscriptionManager.from(this)
}
}
fun Context.createTemporaryThread(message: Message, threadId: Long = generateRandomId()) {
val simpleContactHelper = SimpleContactsHelper(this)
val addresses = message.participants.getAddresses()
val photoUri = if (addresses.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(addresses.first()) else ""
val conversation = Conversation(
threadId = threadId,
snippet = message.body,
date = message.date,
read = true,
title = message.participants.getThreadTitle(),
photoUri = photoUri,
isGroupConversation = addresses.size > 1,
phoneNumber = addresses.first(),
isScheduled = true
)
try {
conversationsDB.insertOrUpdate(conversation)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun Context.updateScheduledMessagesThreadId(messages: List<Message>, newThreadId: Long) {
val scheduledMessages = messages.map { it.copy(threadId = newThreadId) }.toTypedArray()
messagesDB.insertMessages(*scheduledMessages)
}
fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List<Message>? = null) {
val messages = messagesToDelete ?: messagesDB.getScheduledThreadMessages(threadId)
try {
messages.filter { it.isScheduled && it.millis() < System.currentTimeMillis() }.forEach { msg ->
messagesDB.delete(msg.id)
}
if (messages.filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() }.isEmpty()) {
// delete empty temporary thread
val conversation = conversationsDB.getConversationWithThreadId(threadId)
if (conversation != null && conversation.isScheduled) {
conversationsDB.deleteThreadId(threadId)
}
}
} catch (e: Exception) {
e.printStackTrace()
return
}
}

View File

@ -89,7 +89,7 @@ fun Context.isLongMmsMessage(text: String): Boolean {
}
/** Not to be used with real messages persisted in the telephony db. This is for internal use only (e.g. scheduled messages). */
fun generateRandomMessageId(length: Int = 8): Long {
fun generateRandomId(length: Int = 9): Long {
val millis = DateTime.now(DateTimeZone.UTC).millis
val random = abs(Random(millis).nextLong())
return random.toString().takeLast(length).toLong()

View File

@ -14,6 +14,9 @@ interface ConversationsDao {
@Query("SELECT * FROM conversations")
fun getAll(): List<Conversation>
@Query("SELECT * FROM conversations where thread_id = :threadId")
fun getConversationWithThreadId(threadId: Long): Conversation?
@Query("SELECT * FROM conversations WHERE read = 0")
fun getUnreadConversations(): List<Conversation>

View File

@ -23,10 +23,10 @@ interface MessagesDao {
@Query("SELECT * FROM messages WHERE thread_id = :threadId")
fun getThreadMessages(threadId: Long): List<Message>
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND is_scheduled")
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND is_scheduled = 1")
fun getScheduledThreadMessages(threadId: Long): List<Message>
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND id = :messageId AND is_scheduled")
@Query("SELECT * FROM messages WHERE thread_id = :threadId AND id = :messageId AND is_scheduled = 1")
fun getScheduledMessageWithId(threadId: Long, messageId: Long): Message
@Query("SELECT * FROM messages WHERE body LIKE :text")

View File

@ -14,5 +14,6 @@ data class Conversation(
@ColumnInfo(name = "title") var title: String,
@ColumnInfo(name = "photo_uri") var photoUri: String,
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean,
@ColumnInfo(name = "phone_number") var phoneNumber: String
@ColumnInfo(name = "phone_number") var phoneNumber: String,
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false
)

View File

@ -7,6 +7,8 @@ import android.os.PowerManager
import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.conversationsDB
import com.simplemobiletools.smsmessenger.extensions.deleteScheduledMessage
import com.simplemobiletools.smsmessenger.extensions.getAddresses
import com.simplemobiletools.smsmessenger.extensions.messagesDB
import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID
@ -33,6 +35,7 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
val message = try {
context.messagesDB.getScheduledMessageWithId(threadId, messageId)
} catch (e: Exception) {
e.printStackTrace()
return
}
@ -41,7 +44,10 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
try {
context.sendMessage(message.body, addresses, message.subscriptionId, attachments)
context.messagesDB.delete(messageId)
// delete temporary conversation and message as it's already persisted to the telephony db now
context.deleteScheduledMessage(messageId)
context.conversationsDB.deleteThreadId(messageId)
refreshMessages()
} catch (e: Exception) {
context.showErrorToast(e)