From ee8130c76710c92f47006d5f48dd590e1bc3d5d0 Mon Sep 17 00:00:00 2001 From: Naveen Date: Wed, 28 Sep 2022 02:05:06 +0530 Subject: [PATCH] Handle conversations with scheduled messages only --- .../smsmessenger/activities/MainActivity.kt | 37 ++++++++-- .../smsmessenger/activities/ThreadActivity.kt | 39 ++++++++--- .../adapters/ConversationsAdapter.kt | 13 ++-- .../smsmessenger/adapters/ThreadAdapter.kt | 21 ++++-- .../databases/MessagesDatabase.kt | 7 +- .../smsmessenger/extensions/Context.kt | 70 ++++++++++++++++++- .../smsmessenger/helpers/Messaging.kt | 2 +- .../interfaces/ConversationsDao.kt | 3 + .../smsmessenger/interfaces/MessagesDao.kt | 4 +- .../smsmessenger/models/Conversation.kt | 3 +- .../receivers/ScheduledMessageReceiver.kt | 8 ++- 11 files changed, 171 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt index b3645070..7047ec34 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt @@ -208,29 +208,51 @@ class MainActivity : SimpleActivity() { setupConversations(conversations) getNewConversations(conversations) } + conversations.forEach { + clearExpiredScheduledMessages(it.threadId) + } } } private fun getNewConversations(cachedConversations: ArrayList) { - 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 { diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt index 11604f6e..9d72bf14 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt index 9a9a47bb..4613f0f1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt @@ -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(conversation_address, conversation_body_short, conversation_date).forEach { it.setTextColor(textColor) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt index 3b845010..4685b4cd 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt @@ -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, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit + activity: SimpleActivity, var messages: ArrayList, 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() + 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 { diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt index e0d46972..72fda67f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt @@ -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") } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt index bfcc129b..03287e98 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -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, newThreadId: Long) { + val scheduledMessages = messages.map { it.copy(threadId = newThreadId) }.toTypedArray() + messagesDB.insertMessages(*scheduledMessages) +} + +fun Context.clearExpiredScheduledMessages(threadId: Long, messagesToDelete: List? = 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 + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt index 8e9be543..b893fc11 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Messaging.kt @@ -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() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt index 534cdc1f..5493eb00 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt @@ -14,6 +14,9 @@ interface ConversationsDao { @Query("SELECT * FROM conversations") fun getAll(): List + @Query("SELECT * FROM conversations where thread_id = :threadId") + fun getConversationWithThreadId(threadId: Long): Conversation? + @Query("SELECT * FROM conversations WHERE read = 0") fun getUnreadConversations(): List diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt index e9d6a3b2..7036167b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt @@ -23,10 +23,10 @@ interface MessagesDao { @Query("SELECT * FROM messages WHERE thread_id = :threadId") fun getThreadMessages(threadId: Long): List - @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 - @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") diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt index fc5698f4..dd4d5f4b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/Conversation.kt @@ -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 ) diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt index e3c41473..bc89a283 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/ScheduledMessageReceiver.kt @@ -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)