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) setupConversations(conversations)
getNewConversations(conversations) getNewConversations(conversations)
} }
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
}
} }
} }
private fun getNewConversations(cachedConversations: ArrayList<Conversation>) { private fun getNewConversations(cachedConversations: ArrayList<Conversation>) {
val privateCursor = getMyContactsCursor(false, true) val privateCursor = getMyContactsCursor(favoritesOnly = false, withPhoneNumbersOnly = true)
ensureBackgroundThread { ensureBackgroundThread {
val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) val privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor)
val conversations = getConversations(privateContacts = privateContacts) val conversations = getConversations(privateContacts = privateContacts)
val scheduledConversations = cachedConversations.filter { it.isScheduled }
val allConversations = conversations.toArrayList().apply {
addAll(scheduledConversations)
}
runOnUiThread { runOnUiThread {
setupConversations(conversations) setupConversations(allConversations)
} }
conversations.forEach { clonedConversation -> 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) conversationsDB.insertOrUpdate(clonedConversation)
cachedConversations.add(clonedConversation) cachedConversations.add(clonedConversation)
} }
} }
cachedConversations.forEach { cachedConversation -> cachedConversations.forEach { cachedConversation ->
if (!conversations.map { it.threadId }.contains(cachedConversation.threadId)) { val threadId = cachedConversation.threadId
conversationsDB.deleteThreadId(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() hideKeyboard()
ConversationsAdapter(this, sortedConversations, conversations_list) { ConversationsAdapter(this, sortedConversations, conversations_list) {
Intent(this, ThreadActivity::class.java).apply { Intent(this, ThreadActivity::class.java).apply {
putExtra(THREAD_ID, (it as Conversation).threadId) val conversation = it as Conversation
putExtra(THREAD_TITLE, it.title) putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
startActivity(this) startActivity(this)
} }
}.apply { }.apply {

View File

@@ -232,6 +232,8 @@ class ThreadActivity : SimpleActivity() {
} catch (e: Exception) { } catch (e: Exception) {
ArrayList() ArrayList()
} }
clearExpiredScheduledMessages(threadId, messages)
messages.removeAll { it.isScheduled && it.millis() < System.currentTimeMillis() }
messages.sortBy { it.date } messages.sortBy { it.date }
if (messages.size > MESSAGES_LIMIT) { if (messages.size > MESSAGES_LIMIT) {
@@ -330,9 +332,13 @@ class ThreadActivity : SimpleActivity() {
val currAdapter = thread_messages_list.adapter val currAdapter = thread_messages_list.adapter
if (currAdapter == null) { if (currAdapter == null) {
ThreadAdapter(this, threadItems, thread_messages_list) { any -> ThreadAdapter(
handleItemClick(any) activity = this,
}.apply { messages = threadItems,
recyclerView = thread_messages_list,
itemClick = { handleItemClick(it) },
onThreadIdUpdate = { threadId = it }
).apply {
thread_messages_list.adapter = this thread_messages_list.adapter = this
} }
@@ -953,8 +959,13 @@ class ThreadActivity : SimpleActivity() {
refreshedSinceSent = false refreshedSinceSent = false
try { try {
ensureBackgroundThread { ensureBackgroundThread {
val messageId = scheduledMessage?.id ?: generateRandomMessageId() val messageId = scheduledMessage?.id ?: generateRandomId()
val message = buildScheduledMessage(text, subscriptionId, messageId) 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) messagesDB.insertOrUpdate(message)
scheduleMessage(message) scheduleMessage(message)
} }
@@ -1140,8 +1151,18 @@ class ThreadActivity : SimpleActivity() {
notificationManager.cancel(threadId.hashCode()) notificationManager.cancel(threadId.hashCode())
} }
val lastMaxId = messages.maxByOrNull { it.id }?.id ?: 0L val newThreadId = getThreadId(participants.getAddresses().toSet())
messages = getMessages(threadId, true) 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 -> 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 // 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() hideScheduleSendUi()
if (scheduledMessage != null) { if (scheduledMessage != null) {
ensureBackgroundThread { ensureBackgroundThread {
messagesDB.delete(scheduledMessage!!.id) deleteScheduledMessage(scheduledMessage!!.id)
refreshMessages() refreshMessages()
} }
} }
@@ -1267,6 +1288,7 @@ class ThreadActivity : SimpleActivity() {
} }
private fun buildScheduledMessage(text: String, subscriptionId: Int, messageId: Long): Message { private fun buildScheduledMessage(text: String, subscriptionId: Int, messageId: Long): Message {
val threadId = if (messages.isEmpty()) messageId else threadId
return Message( return Message(
id = messageId, id = messageId,
body = text, body = text,
@@ -1287,8 +1309,9 @@ class ThreadActivity : SimpleActivity() {
private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment { private fun buildMessageAttachment(text: String, messageId: Long): MessageAttachment {
val attachments = attachmentSelections.values 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() .toArrayList()
return MessageAttachment(messageId, text, attachments) return MessageAttachment(messageId, text, attachments)
} }
} }

View File

@@ -173,7 +173,7 @@ class ConversationsAdapter(
} }
try { try {
conversations.removeAll(conversationsToRemove) conversations.removeAll(conversationsToRemove.toSet())
} catch (ignored: Exception) { } catch (ignored: Exception) {
} }
@@ -319,15 +319,16 @@ class ConversationsAdapter(
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f) setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
} }
if (conversation.read) { val style = if (conversation.read) {
conversation_address.setTypeface(null, Typeface.NORMAL)
conversation_body_short.setTypeface(null, Typeface.NORMAL)
conversation_body_short.alpha = 0.7f conversation_body_short.alpha = 0.7f
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
} else { } else {
conversation_address.setTypeface(null, Typeface.BOLD)
conversation_body_short.setTypeface(null, Typeface.BOLD)
conversation_body_short.alpha = 1f 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 { arrayListOf<TextView>(conversation_address, conversation_body_short, conversation_date).forEach {
it.setTextColor(textColor) 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_error.view.*
import kotlinx.android.synthetic.main.item_thread_sending.view.* import kotlinx.android.synthetic.main.item_thread_sending.view.*
import kotlinx.android.synthetic.main.item_thread_success.view.* import kotlinx.android.synthetic.main.item_thread_success.view.*
import java.util.*
class ThreadAdapter( 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) { ) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
private var fontSize = activity.getTextSize() private var fontSize = activity.getTextSize()
@@ -204,11 +205,21 @@ class ThreadAdapter(
messagesToRemove.forEach { messagesToRemove.forEach {
activity.deleteMessage((it as Message).id, it.isMMS) activity.deleteMessage((it as Message).id, it.isMMS)
} }
messages.removeAll(messagesToRemove) messages.removeAll(messagesToRemove.toSet())
activity.updateLastConversationMessage(threadId) 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 { activity.runOnUiThread {
if (messages.filter { it is Message }.isEmpty()) { if (messages.isEmpty()) {
activity.finish() activity.finish()
} else { } else {
removeSelectedItems(positions) removeSelectedItems(positions)
@@ -333,7 +344,7 @@ class ThreadAdapter(
thread_message_scheduled_icon.beGone() thread_message_scheduled_icon.beGone()
thread_message_body.setPadding(padding, padding, padding, padding) 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) { private fun launchViewIntent(uri: Uri, mimetype: String, filename: String) {
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(uri, mimetype.toLowerCase()) setDataAndType(uri, mimetype.lowercase(Locale.getDefault()))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try { try {

View File

@@ -67,8 +67,10 @@ abstract class MessagesDatabase : RoomDatabase() {
database.apply { 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("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) " + execSQL(
"SELECT thread_id, snippet, date, read, title, photo_uri, is_group_conversation, phone_number FROM conversations") "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") execSQL("DROP TABLE conversations")
@@ -91,6 +93,7 @@ abstract class MessagesDatabase : RoomDatabase() {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.apply { database.apply {
execSQL("ALTER TABLE messages ADD COLUMN is_scheduled INTEGER NOT NULL DEFAULT 0") 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)) messages.addAll(getMMS(threadId, getImageResolutions, sortOrder))
if (includeScheduledMessages) { if (includeScheduledMessages) {
val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId) try {
messages.addAll(scheduledMessages) val scheduledMessages = messagesDB.getScheduledThreadMessages(threadId)
messages.addAll(scheduledMessages)
} catch (e: Exception) {
e.printStackTrace()
}
} }
messages = messages messages = messages
@@ -580,7 +584,11 @@ fun Context.deleteConversation(threadId: Long) {
} }
uri = Mms.CONTENT_URI uri = Mms.CONTENT_URI
contentResolver.delete(uri, selection, selectionArgs) try {
contentResolver.delete(uri, selection, selectionArgs)
} catch (e: Exception) {
e.printStackTrace()
}
conversationsDB.deleteThreadId(threadId) conversationsDB.deleteThreadId(threadId)
messagesDB.deleteThreadMessages(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) { fun Context.markMessageRead(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
@@ -1001,3 +1017,51 @@ fun Context.subscriptionManagerCompat(): SubscriptionManager {
SubscriptionManager.from(this) 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). */ /** 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 millis = DateTime.now(DateTimeZone.UTC).millis
val random = abs(Random(millis).nextLong()) val random = abs(Random(millis).nextLong())
return random.toString().takeLast(length).toLong() return random.toString().takeLast(length).toLong()

View File

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

View File

@@ -23,10 +23,10 @@ interface MessagesDao {
@Query("SELECT * FROM messages WHERE thread_id = :threadId") @Query("SELECT * FROM messages WHERE thread_id = :threadId")
fun getThreadMessages(threadId: Long): List<Message> 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> 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 fun getScheduledMessageWithId(threadId: Long, messageId: Long): Message
@Query("SELECT * FROM messages WHERE body LIKE :text") @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 = "title") var title: String,
@ColumnInfo(name = "photo_uri") var photoUri: String, @ColumnInfo(name = "photo_uri") var photoUri: String,
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean, @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.extensions.showErrorToast
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R 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.getAddresses
import com.simplemobiletools.smsmessenger.extensions.messagesDB import com.simplemobiletools.smsmessenger.extensions.messagesDB
import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID import com.simplemobiletools.smsmessenger.helpers.SCHEDULED_MESSAGE_ID
@@ -33,6 +35,7 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
val message = try { val message = try {
context.messagesDB.getScheduledMessageWithId(threadId, messageId) context.messagesDB.getScheduledMessageWithId(threadId, messageId)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace()
return return
} }
@@ -41,7 +44,10 @@ class ScheduledMessageReceiver : BroadcastReceiver() {
try { try {
context.sendMessage(message.body, addresses, message.subscriptionId, attachments) 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() refreshMessages()
} catch (e: Exception) { } catch (e: Exception) {
context.showErrorToast(e) context.showErrorToast(e)