diff --git a/common/src/main/java/com/moez/QKSMS/compat/TelephonyCompat.kt b/common/src/main/java/com/moez/QKSMS/compat/TelephonyCompat.kt index 56e5b77c..d412074b 100644 --- a/common/src/main/java/com/moez/QKSMS/compat/TelephonyCompat.kt +++ b/common/src/main/java/com/moez/QKSMS/compat/TelephonyCompat.kt @@ -19,46 +19,46 @@ package dev.octoshrimpy.quik.compat import android.content.Context -import android.database.sqlite.SqliteWrapper import android.net.Uri -import android.os.Build import android.provider.BaseColumns import android.provider.Telephony import android.text.TextUtils import android.util.Patterns -import timber.log.Timber import java.util.regex.Pattern -object TelephonyCompat { +class TelephonyCompat { + companion object { + val THREADS_CONTENT_URI = Telephony.Threads.CONTENT_URI - private val ID_PROJECTION = arrayOf(BaseColumns._ID) + private val ID_PROJECTION = arrayOf(BaseColumns._ID) - private val THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID") + private val THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID") - val NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*") + val NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*") - fun getOrCreateThreadId(context: Context, recipient: String): Long { - return getOrCreateThreadId(context, listOf(recipient)) - } - - fun getOrCreateThreadId(context: Context, recipients: Collection): Long { - return Telephony.Threads.getOrCreateThreadId(context, recipients.toSet()) - } - - fun extractAddrSpec(address: String): String { - val match = NAME_ADDR_EMAIL_PATTERN.matcher(address) - return if (match.matches()) { - match.group(2) - } else address - } - - fun isEmailAddress(address: String): Boolean { - if (TextUtils.isEmpty(address)) { - return false + fun getOrCreateThreadId(context: Context, recipient: String): Long { + return getOrCreateThreadId(context, listOf(recipient)) + } + + fun getOrCreateThreadId(context: Context, recipients: Collection): Long { + return Telephony.Threads.getOrCreateThreadId(context, recipients.toSet()) + } + + fun extractAddrSpec(address: String): String { + val match = NAME_ADDR_EMAIL_PATTERN.matcher(address) + return if (match.matches()) { + match.group(2) + } else address + } + + fun isEmailAddress(address: String): Boolean { + if (TextUtils.isEmpty(address)) { + return false + } + val s = extractAddrSpec(address) + val match = Patterns.EMAIL_ADDRESS.matcher(s) + return match.matches() } - val s = extractAddrSpec(address) - val match = Patterns.EMAIL_ADDRESS.matcher(s) - return match.matches() } } \ No newline at end of file diff --git a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt index 0827edc6..11da4d2f 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt @@ -20,8 +20,6 @@ package dev.octoshrimpy.quik.repository import android.content.ContentUris import android.content.Context -import android.provider.Telephony -import android.view.View.OnLongClickListener import dev.octoshrimpy.quik.compat.TelephonyCompat import dev.octoshrimpy.quik.extensions.anyOf import dev.octoshrimpy.quik.extensions.asObservable @@ -66,21 +64,21 @@ class ConversationRepositoryImpl @Inject constructor( } return Realm.getDefaultInstance() - .where(Conversation::class.java) - .notEqualTo("id", 0L) - .equalTo("archived", archived) - .equalTo("blocked", false) - .isNotEmpty("recipients") - .beginGroup() - .isNotNull("lastMessage") - .or() - .isNotEmpty("draft") - .endGroup() - .sort( - sortOrder.toTypedArray(), - sortDirections.toTypedArray() - ) - .findAllAsync() + .where(Conversation::class.java) + .notEqualTo("id", 0L) + .equalTo("archived", archived) + .equalTo("blocked", false) + .isNotEmpty("recipients") + .beginGroup() + .isNotNull("lastMessage") + .or() + .isNotEmpty("draft") + .endGroup() + .sort( + sortOrder.toTypedArray(), + sortDirections.toTypedArray() + ) + .findAllAsync() } override fun getConversationsSnapshot(unreadAtTop: Boolean): List { @@ -94,60 +92,49 @@ class ConversationRepositoryImpl @Inject constructor( return Realm.getDefaultInstance().use { realm -> realm.refresh() - realm.copyFromRealm(realm.where(Conversation::class.java) - .notEqualTo("id", 0L) - .equalTo("archived", false) - .equalTo("blocked", false) - .isNotEmpty("recipients") - .beginGroup() - .isNotNull("lastMessage") - .or() - .isNotEmpty("draft") - .endGroup() - .sort( - sortOrder.toTypedArray(), - sortDirections.toTypedArray() - ) - .findAll()) + realm.where(Conversation::class.java) + .notEqualTo("id", 0L) + .equalTo("archived", false) + .equalTo("blocked", false) + .isNotEmpty("recipients") + .beginGroup() + .isNotNull("lastMessage") + .or() + .isNotEmpty("draft") + .endGroup() + .sort(sortOrder.toTypedArray(), sortDirections.toTypedArray()) + .findAll() + .let(realm::copyFromRealm) } } - override fun getTopConversations(): List { - return Realm.getDefaultInstance().use { realm -> - realm.copyFromRealm(realm.where(Conversation::class.java) - .notEqualTo("id", 0L) - .isNotNull("lastMessage") - .beginGroup() - .equalTo("pinned", true) - .or() - .greaterThan("lastMessage.date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) - .endGroup() - .equalTo("archived", false) - .equalTo("blocked", false) - .isNotEmpty("recipients") - .findAll()) - .sortedWith(compareByDescending { conversation -> conversation.pinned } - .thenByDescending { conversation -> - realm.where(Message::class.java) - .equalTo("threadId", conversation.id) - .greaterThan("date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) - .count() - }) + override fun getTopConversations() = + Realm.getDefaultInstance().use { realm -> + realm.where(Conversation::class.java) + .notEqualTo("id", 0L) + .isNotNull("lastMessage") + .beginGroup() + .equalTo("pinned", true) + .or() + .greaterThan("lastMessage.date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) + .endGroup() + .equalTo("archived", false) + .equalTo("blocked", false) + .isNotEmpty("recipients") + .findAll() + .let(realm::copyFromRealm) + .sortedWith(compareByDescending { conversation -> conversation.pinned } + .thenByDescending { conversation -> + realm.where(Message::class.java) + .equalTo("threadId", conversation.id) + .greaterThan("date", System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) + .count() + } + ) } - } -// override fun setConversationName(id: Long, name: String) { -// Realm.getDefaultInstance().use { realm -> -// realm.executeTransaction { -// realm.where(Conversation::class.java) -// .equalTo("id", id) -// .findFirst() -// ?.name = name -// } -// } -// } - override fun setConversationName(id: Long, name: String): Completable { - return Completable.fromAction { + override fun setConversationName(id: Long, name: String) = + Completable.fromAction { Realm.getDefaultInstance().use { realm -> realm.executeTransaction { realm.where(Conversation::class.java) @@ -157,150 +144,134 @@ class ConversationRepositoryImpl @Inject constructor( } } }.subscribeOn(Schedulers.io()) // Ensure the operation is performed on a background thread - } override fun searchConversations(query: CharSequence): List { val realm = Realm.getDefaultInstance() val normalizedQuery = query.removeAccents() val conversations = realm.copyFromRealm(realm - .where(Conversation::class.java) - .notEqualTo("id", 0L) - .isNotNull("lastMessage") - .equalTo("blocked", false) - .isNotEmpty("recipients") - .sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING) - .findAll()) + .where(Conversation::class.java) + .notEqualTo("id", 0L) + .isNotNull("lastMessage") + .equalTo("blocked", false) + .isNotEmpty("recipients") + .sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING) + .findAll()) val messagesByConversation = realm.copyFromRealm(realm - .where(Message::class.java) - .beginGroup() - .contains("body", normalizedQuery, Case.INSENSITIVE) - .or() - .contains("parts.text", normalizedQuery, Case.INSENSITIVE) - .endGroup() - .findAll()) - .asSequence() - .groupBy { message -> message.threadId } - .filter { (threadId, _) -> conversations.firstOrNull { it.id == threadId } != null } - .map { (threadId, messages) -> Pair(conversations.first { it.id == threadId }, messages.size) } - .map { (conversation, messages) -> SearchResult(normalizedQuery, conversation, messages) } - .sortedByDescending { result -> result.messages } - .toList() + .where(Message::class.java) + .beginGroup() + .contains("body", normalizedQuery, Case.INSENSITIVE) + .or() + .contains("parts.text", normalizedQuery, Case.INSENSITIVE) + .endGroup() + .findAll()) + .asSequence() + .groupBy { message -> message.threadId } + .filter { (threadId, _) -> conversations.firstOrNull { it.id == threadId } != null } + .map { (threadId, messages) -> Pair(conversations.first { it.id == threadId }, messages.size) } + .map { (conversation, messages) -> SearchResult(normalizedQuery, conversation, messages) } + .sortedByDescending { result -> result.messages } + .toList() realm.close() return conversations - .filter { conversation -> conversationFilter.filter(conversation, normalizedQuery) } - .map { conversation -> SearchResult(normalizedQuery, conversation, 0) } + messagesByConversation + .filter { conversation -> conversationFilter.filter(conversation, normalizedQuery) } + .map { + conversation -> SearchResult(normalizedQuery, conversation, 0) + } + messagesByConversation } - override fun getBlockedConversations(): RealmResults { - return Realm.getDefaultInstance() - .where(Conversation::class.java) - .equalTo("blocked", true) - .sort( - arrayOf("lastMessage.date"), - arrayOf(Sort.DESCENDING) - ) - .findAll() - } + override fun getBlockedConversations(): RealmResults = + Realm.getDefaultInstance() + .where(Conversation::class.java) + .equalTo("blocked", true) + .sort( + arrayOf("lastMessage.date"), + arrayOf(Sort.DESCENDING) + ) + .findAll() - override fun getBlockedConversationsAsync(): RealmResults { - return Realm.getDefaultInstance() + override fun getBlockedConversationsAsync(): RealmResults = + Realm.getDefaultInstance() + .where(Conversation::class.java) + .equalTo("blocked", true) + .sort( + arrayOf("lastMessage.date"), + arrayOf(Sort.DESCENDING) + ) + .findAllAsync() + + override fun getConversationAsync(threadId: Long): Conversation = + Realm.getDefaultInstance() + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirstAsync() + + override fun getConversation(threadId: Long) = + Realm.getDefaultInstance() + .apply { refresh() } + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirst() + + override fun getUnseenIds(archived: Boolean) = + ArrayList().apply { + Realm.getDefaultInstance() .where(Conversation::class.java) - .equalTo("blocked", true) + .notEqualTo("id", 0L) + .equalTo("archived", archived) + .equalTo("blocked", false) + .equalTo("lastMessage.seen", false) .sort( arrayOf("lastMessage.date"), arrayOf(Sort.DESCENDING) ) .findAllAsync() - } - - override fun getConversationAsync(threadId: Long): Conversation { - return Realm.getDefaultInstance() - .where(Conversation::class.java) - .equalTo("id", threadId) - .findFirstAsync() - } - - override fun getConversation(threadId: Long): Conversation? { - return Realm.getDefaultInstance() - .apply { refresh() } - .where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() - } - - override fun getUnseenIds(archived: Boolean): List { - val conversationIds = ArrayList() - - Realm.getDefaultInstance() - .where(Conversation::class.java) - .notEqualTo("id", 0L) - .equalTo("archived", archived) - .equalTo("blocked", false) - .equalTo("lastMessage.seen", false) - .sort( - arrayOf("lastMessage.date"), - arrayOf(Sort.DESCENDING) - ) - .findAllAsync() - .forEach { conversation -> conversationIds.add(conversation.id) } - - return conversationIds - } - - override fun getUnreadIds(archived: Boolean): List { - val conversationIds = ArrayList() - - Realm.getDefaultInstance() - .where(Conversation::class.java) - .notEqualTo("id", 0L) - .equalTo("archived", archived) - .equalTo("blocked", false) - .equalTo("lastMessage.read", false) - .sort( - arrayOf("lastMessage.date"), - arrayOf(Sort.DESCENDING) - ) - .findAllAsync() - .forEach { conversation -> conversationIds.add(conversation.id) } - - return conversationIds - } - - override fun getConversationAndLastSenderContactName(threadId: Long): Pair? { - val conversation = Realm.getDefaultInstance() - .apply { refresh() } - .where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() - - if (conversation === null) - return null - - var conversationLastSmsSender: String? = null - - if (conversation !== null) { - conversationLastSmsSender = conversation?.recipients?.find { recipient -> - phoneNumberUtils.compare(recipient.address, conversation.lastMessage!!.address) - }?.contact?.name + .forEach { conversation -> add(conversation.id) } } - return return Pair(conversation, conversationLastSmsSender) - } - override fun getConversations(vararg threadIds: Long): RealmResults { - return Realm.getDefaultInstance() + override fun getUnreadIds(archived: Boolean) = + ArrayList().apply { + Realm.getDefaultInstance() .where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() - } + .notEqualTo("id", 0L) + .equalTo("archived", archived) + .equalTo("blocked", false) + .equalTo("lastMessage.read", false) + .sort( + arrayOf("lastMessage.date"), + arrayOf(Sort.DESCENDING) + ) + .findAllAsync() + .forEach { conversation -> add(conversation.id) } + } - override fun getUnmanagedConversations(): Observable> { - val realm = Realm.getDefaultInstance() - return realm.where(Conversation::class.java) + override fun getConversationAndLastSenderContactName(threadId: Long): Pair? = + Realm.getDefaultInstance() + .apply { refresh() } + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirst() + ?.let { conversation -> + val conversationLastSmsSender: String? = conversation.recipients.find { recipient -> + phoneNumberUtils.compare(recipient.address, conversation.lastMessage!!.address) + }?.contact?.name + + Pair(conversation, conversationLastSmsSender) + } + + override fun getConversations(vararg threadIds: Long): RealmResults = + Realm.getDefaultInstance() + .where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() + + override fun getUnmanagedConversations(): Observable> = + Realm.getDefaultInstance().let { realm-> + realm.where(Conversation::class.java) .sort("lastMessage.date", Sort.DESCENDING) .notEqualTo("id", 0L) .isNotNull("lastMessage") @@ -315,164 +286,138 @@ class ConversationRepositoryImpl @Inject constructor( .map { realm.copyFromRealm(it) } .subscribeOn(AndroidSchedulers.mainThread()) .observeOn(Schedulers.io()) - } + } - override fun getRecipients(): RealmResults { - val realm = Realm.getDefaultInstance() - return realm.where(Recipient::class.java) + override fun getRecipients(): RealmResults = + Realm.getDefaultInstance() + .where(Recipient::class.java) .findAll() - } - override fun getUnmanagedRecipients(): Observable> { - val realm = Realm.getDefaultInstance() - return realm.where(Recipient::class.java) + override fun getUnmanagedRecipients(): Observable> = + Realm.getDefaultInstance().let { realm -> + realm.where(Recipient::class.java) .isNotNull("contact") .findAllAsync() .asObservable() .filter { it.isLoaded && it.isValid } .map { realm.copyFromRealm(it) } .subscribeOn(AndroidSchedulers.mainThread()) - } + } - override fun getRecipient(recipientId: Long): Recipient? { - return Realm.getDefaultInstance() - .where(Recipient::class.java) - .equalTo("id", recipientId) - .findFirst() - } + override fun getRecipient(recipientId: Long): Recipient? = + Realm.getDefaultInstance() + .where(Recipient::class.java) + .equalTo("id", recipientId) + .findFirst() - override fun getThreadId(recipient: String): Long? { - return getThreadId(listOf(recipient)) - } + override fun getConversation(recipient: String) = + getConversation(listOf(recipient)) - override fun getThreadId(recipients: Collection): Long? { - return Realm.getDefaultInstance().use { realm -> + override fun getConversation(recipients: Collection) = + Realm.getDefaultInstance().use { realm -> realm.refresh() realm.where(Conversation::class.java) - .findAll() - .asSequence() - .filter { conversation -> conversation.recipients.size == recipients.size } - .find { conversation -> - conversation.recipients.map { it.address }.all { address -> - recipients.any { recipient -> phoneNumberUtils.compare(recipient, address) } - } - }?.id - } - } - - override fun getOrCreateConversation(threadId: Long): Conversation? { - return getConversation(threadId) ?: getConversationFromCp(threadId) - } - - override fun getOrCreateConversation(address: String): Conversation? { - return getOrCreateConversation(listOf(address)) - } - - override fun getOrCreateConversation(addresses: List): Conversation? { - if (addresses.isEmpty()) { - return null - } - - return (getThreadId(addresses) ?: tryOrNull { TelephonyCompat.getOrCreateThreadId(context, addresses.toSet()) }) - ?.takeIf { threadId -> threadId != 0L } - ?.let { threadId -> - getConversation(threadId) - ?.let(Realm.getDefaultInstance()::copyFromRealm) - ?: getConversationFromCp(threadId) + .findAll() + .asSequence() + .filter { conversation -> conversation.recipients.size == recipients.size } + .find { conversation -> + conversation.recipients.map { it.address }.all { address -> + recipients.any { recipient -> phoneNumberUtils.compare(recipient, address) } + } } - } + } - override fun saveDraft(threadId: Long, draft: String) { + override fun getOrCreateConversation(threadId: Long) = + tryOrNull(true) { + getConversation(threadId) ?: createConversationFromCp(threadId) + } + + override fun getOrCreateConversation(address: String) = + getOrCreateConversation(listOf(address)) + + override fun getOrCreateConversation(addresses: Collection) = + tryOrNull(true) { + getConversation(addresses) + ?: tryOrNull { TelephonyCompat.getOrCreateThreadId(context, addresses.toSet()) } + ?.takeIf { it != 0L } + ?.let { threadId -> getOrCreateConversation(threadId) } + } + + override fun saveDraft(threadId: Long, draft: String) = Realm.getDefaultInstance().use { realm -> realm.refresh() val conversation = realm.where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() + .equalTo("id", threadId) + .findFirst() realm.executeTransaction { conversation?.takeIf { it.isValid }?.draft = draft } } - } - override fun updateConversations(vararg threadIds: Long) { + override fun updateConversations(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> realm.refresh() threadIds.forEach { threadId -> val conversation = realm - .where(Conversation::class.java) - .equalTo("id", threadId) - .findFirst() ?: return + .where(Conversation::class.java) + .equalTo("id", threadId) + .findFirst() ?: return@forEach val message = realm - .where(Message::class.java) - .equalTo("threadId", threadId) - .sort("date", Sort.DESCENDING) - .findFirst() + .where(Message::class.java) + .equalTo("threadId", threadId) + .sort("date", Sort.DESCENDING) + .findFirst() - realm.executeTransaction { - conversation.lastMessage = message - } + realm.executeTransaction { conversation.lastMessage = message } } } - } - override fun markArchived(vararg threadIds: Long) { + override fun markArchived(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { - conversations.forEach { it.archived = true } - } + realm.executeTransaction { conversations.forEach { it.archived = true } } } - } - override fun markUnarchived(vararg threadIds: Long) { + override fun markUnarchived(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { - conversations.forEach { it.archived = false } - } + realm.executeTransaction { conversations.forEach { it.archived = false } } } - } - override fun markPinned(vararg threadIds: Long) { + override fun markPinned(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { - conversations.forEach { it.pinned = true } - } + realm.executeTransaction { conversations.forEach { it.pinned = true } } } - } - override fun markUnpinned(vararg threadIds: Long) { + override fun markUnpinned(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() + .anyOf("id", threadIds) + .findAll() - realm.executeTransaction { - conversations.forEach { it.pinned = false } - } + realm.executeTransaction { conversations.forEach { it.pinned = false } } } - } - override fun markBlocked(threadIds: List, blockingClient: Int, blockReason: String?) { + override fun markBlocked(threadIds: Collection, blockingClient: Int, blockReason: String?) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds.toLongArray()) - .equalTo("blocked", false) - .findAll() + .anyOf("id", threadIds.toLongArray()) + .equalTo("blocked", false) + .findAll() realm.executeTransaction { conversations.forEach { conversation -> @@ -482,13 +427,12 @@ class ConversationRepositoryImpl @Inject constructor( } } } - } - override fun markUnblocked(vararg threadIds: Long) { + override fun markUnblocked(vararg threadIds: Long) = Realm.getDefaultInstance().use { realm -> val conversations = realm.where(Conversation::class.java) - .anyOf("id", threadIds) - .findAll() + .anyOf("id", threadIds) + .findAll() realm.executeTransaction { conversations.forEach { conversation -> @@ -498,12 +442,15 @@ class ConversationRepositoryImpl @Inject constructor( } } } - } override fun deleteConversations(vararg threadIds: Long) { Realm.getDefaultInstance().use { realm -> - val conversation = realm.where(Conversation::class.java).anyOf("id", threadIds).findAll() - val messages = realm.where(Message::class.java).anyOf("threadId", threadIds).findAll() + val conversation = realm.where(Conversation::class.java) + .anyOf("id", threadIds) + .findAll() + val messages = realm.where(Message::class.java) + .anyOf("threadId", threadIds) + .findAll() realm.executeTransaction { conversation.deleteAllFromRealm() @@ -511,9 +458,12 @@ class ConversationRepositoryImpl @Inject constructor( } } - threadIds.forEach { threadId -> - val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId) - context.contentResolver.delete(uri, null, null) + threadIds.forEach { + context.contentResolver.delete( + ContentUris.withAppendedId(TelephonyCompat.THREADS_CONTENT_URI, it), + null, + null + ) } } @@ -524,40 +474,45 @@ class ConversationRepositoryImpl @Inject constructor( * we can return a [Conversation]. On some devices, the ContentProvider won't return the * conversation unless it contains at least 1 message */ - private fun getConversationFromCp(threadId: Long): Conversation? { - return cursorToConversation.getConversationsCursor() + private fun createConversationFromCp(threadId: Long) = + tryOrNull(true) { + cursorToConversation.getConversationsCursor() ?.map(cursorToConversation::map) - ?.firstOrNull { it.id == threadId } - ?.let { conversation -> - val realm = Realm.getDefaultInstance() - val contacts = realm.copyFromRealm(realm.where(Contact::class.java).findAll()) - val lastMessage = realm.where(Message::class.java).equalTo("threadId", threadId) - .sort("date", Sort.DESCENDING).findFirst()?.let(realm::copyFromRealm) + ?.firstOrNull { conversation -> conversation.id == threadId } + ?.also { conversation -> + Realm.getDefaultInstance().use { realm -> + val realmContacts = realm.where(Contact::class.java).findAll() - val recipients = conversation.recipients - .map { recipient -> recipient.id } - .map { id -> cursorToRecipient.getRecipientCursor(id) } - .mapNotNull { recipientCursor -> - // Map the recipient cursor to a list of recipients - recipientCursor?.use { recipientCursor.map { cursorToRecipient.map(recipientCursor) } } + // match recipients from provider to recipients in realm + val matchedRecipients = conversation.recipients + .mapNotNull { recipient -> + // map the recipient cursor to a list of recipients + cursorToRecipient.getRecipientCursor(recipient.id)?.use { cursor -> + cursor.map { cursorToRecipient.map(it) } + } } .flatten() .map { recipient -> recipient.apply { - contact = contacts.firstOrNull { - it.numbers.any { phoneNumberUtils.compare(recipient.address, it.address) } + contact = realmContacts.firstOrNull { realmContact -> + realmContact.numbers.any { + phoneNumberUtils.compare(it.address, address) + } } } } - conversation.recipients.clear() - conversation.recipients.addAll(recipients) - conversation.lastMessage = lastMessage - realm.executeTransaction { it.insertOrUpdate(conversation) } - realm.close() + conversation.apply { + recipients.clear() + recipients.addAll(matchedRecipients) + lastMessage = realm.where(Message::class.java) + .equalTo("threadId", threadId) + .sort("date", Sort.DESCENDING) + .findFirst() + } - conversation + realm.executeTransaction { it.insertOrUpdate(conversation) } + } } - } - + } } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/SendMessage.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SendMessage.kt index 23fc11d8..7932894f 100644 --- a/domain/src/main/java/com/moez/QKSMS/interactor/SendMessage.kt +++ b/domain/src/main/java/com/moez/QKSMS/interactor/SendMessage.kt @@ -19,8 +19,6 @@ package dev.octoshrimpy.quik.interactor import android.content.Context -import dev.octoshrimpy.quik.compat.TelephonyCompat -import dev.octoshrimpy.quik.extensions.mapNotNull import dev.octoshrimpy.quik.model.Attachment import dev.octoshrimpy.quik.repository.ConversationRepository import dev.octoshrimpy.quik.repository.MessageRepository @@ -44,29 +42,29 @@ class SendMessage @Inject constructor( ) override fun buildObservable(params: Params): Flowable<*> = Flowable.just(Unit) - .filter { params.addresses.isNotEmpty() } - .doOnNext { - // If a threadId isn't provided, try to obtain one - val threadId = when (params.threadId) { - 0L -> TelephonyCompat.getOrCreateThreadId(context, params.addresses.toSet()) - else -> params.threadId - } - messageRepo.sendMessage(params.subId, threadId, params.addresses, params.body, params.attachments, - params.delay) + .filter { params.addresses.isNotEmpty() } + .doOnNext { + // If a threadId isn't provided, try to obtain one + val threadId = when (params.threadId) { + 0L -> conversationRepo.getOrCreateConversation(params.addresses)?.id ?: 0 + else -> params.threadId + } - // delete attachment local files, if any, because they're saved to mms db by now - params.attachments.forEach { it.removeCacheFile() } + // if unable to find or create a conversation (and/or underlying threadId), fail + if (threadId == 0L) + return@doOnNext + + params.apply { + messageRepo.sendMessage(subId, threadId, addresses, body, attachments, delay) } - .mapNotNull { - // If the threadId wasn't provided, then it's probably because it doesn't exist in Realm. - // Sync it now and get the id - when (params.threadId) { - 0L -> conversationRepo.getOrCreateConversation(params.addresses)?.id - else -> params.threadId - } - } - .doOnNext { threadId -> conversationRepo.updateConversations(threadId) } - .doOnNext { threadId -> conversationRepo.markUnarchived(threadId) } - .flatMap { updateBadge.buildObservable(Unit) } // Update the widget + + conversationRepo.updateConversations(threadId) + + conversationRepo.markUnarchived(threadId) + + // delete attachment local files, if any, because they're saved to mms db by now + params.attachments.forEach { it.removeCacheFile() } + } + .flatMap { updateBadge.buildObservable(Unit) } // Update the widget } \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt index 2a34b72f..400eca1d 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt @@ -67,15 +67,15 @@ interface ConversationRepository { fun getRecipient(recipientId: Long): Recipient? - fun getThreadId(recipient: String): Long? + fun getConversation(recipient: String): Conversation? - fun getThreadId(recipients: Collection): Long? + fun getConversation(recipients: Collection): Conversation? fun getOrCreateConversation(threadId: Long): Conversation? fun getOrCreateConversation(address: String): Conversation? - fun getOrCreateConversation(addresses: List): Conversation? + fun getOrCreateConversation(addresses: Collection): Conversation? fun saveDraft(threadId: Long, draft: String) @@ -92,7 +92,7 @@ interface ConversationRepository { fun markUnpinned(vararg threadIds: Long) - fun markBlocked(threadIds: List, blockingClient: Int, blockReason: String?) + fun markBlocked(threadIds: Collection, blockingClient: Int, blockReason: String?) fun markUnblocked(vararg threadIds: Long) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt index 8ada8c8a..1045aeb9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt @@ -42,8 +42,8 @@ class BlockedNumbersPresenter @Inject constructor( .observeOn(Schedulers.io()) .doOnNext { id -> blockingRepo.getBlockedNumber(id)?.address - ?.let(conversationRepo::getThreadId) - ?.let { threadId -> markUnblocked.execute(listOf(threadId)) } + ?.let(conversationRepo::getConversation) + ?.let { conversation -> markUnblocked.execute(listOf(conversation.id)) } } .doOnNext(blockingRepo::unblockNumber) .subscribeOn(Schedulers.io()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index 282b59e3..a7891fb8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -156,56 +156,39 @@ class ComposeViewModel @Inject constructor( newState { copy(attachments = sharedAttachments) } val initialConversation = threadId.takeIf { it != 0L } - ?.let(conversationRepo::getConversationAsync) - ?.asObservable() - ?: Observable.empty() + ?.let(conversationRepo::getConversationAsync) + ?.asObservable() + ?: Observable.empty() val selectedConversation = selectedChips - .skipWhile { it.isEmpty() } - .map { chips -> chips.map { it.address } } - .distinctUntilChanged() - .doOnNext { newState { copy(loading = true) } } - .observeOn(Schedulers.io()) - .map { addresses -> Pair(conversationRepo.getOrCreateConversation(addresses)?.id ?: 0, addresses) } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { newState { copy(loading = false) } } - .switchMap { (threadId, addresses) -> - // If we already have this thread in realm, or we're able to obtain it from the - // system, just return that. - threadId.takeIf { it > 0 }?.let { - return@switchMap conversationRepo.getConversationAsync(threadId).asObservable() - } - - // Otherwise, we'll monitor the conversations until our expected conversation is created - conversationRepo.getConversations(prefs.unreadAtTop.get()).asObservable() - .filter { it.isLoaded } - .observeOn(Schedulers.io()) - .map { conversationRepo.getOrCreateConversation(addresses)?.id ?: 0 } - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { actualThreadId -> - when (actualThreadId) { - 0L -> Observable.just(Conversation(0)) - else -> conversationRepo.getConversationAsync(actualThreadId).asObservable() - } - } + .skipWhile { recipients -> recipients.isEmpty() } + .map { recipients -> recipients.map { it.address } } + .distinctUntilChanged() + .doOnNext { newState { copy(loading = true) } } + .observeOn(Schedulers.io()) // background thread for possible long telephony running + .doOnNext { addresses -> conversationRepo.getOrCreateConversation(addresses) } + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { addresses -> + // monitors convos and triggers when wanted convo is present + conversationRepo.getConversations(false) + .asObservable() + .filter { conversations -> conversations.isLoaded } + .mapNotNull { conversationRepo.getConversation(addresses) } + .doOnNext { newState { copy(loading = false) } } + .switchMap { conversation -> conversation.asObservable() } } - // Merges two potential conversation sources (threadId from constructor and contact selection) into a single - // stream of conversations. If the conversation was deleted, notify the activity to shut down + // Merges two potential conversation sources (constructor threadId and contact selection) + // into a single stream of conversations. If the conversation was deleted, notify the + // activity to shut down disposables += selectedConversation - .mergeWith(initialConversation) - .filter { conversation -> conversation.isLoaded } - .doOnNext { conversation -> - if (!conversation.isValid) { - newState { copy(hasError = true) } - } - } - .filter { conversation -> conversation.isValid } - .subscribe(conversation::onNext) + .mergeWith(initialConversation) + .filter { it.isLoaded } + .filter { it.isValid.also { if (!it) newState { copy(hasError = true) } } } + .subscribe(conversation::onNext) - if (addresses.isNotEmpty()) { + if (addresses.isNotEmpty()) selectedChips.onNext(addresses.map { address -> Recipient(address = address) }) - } disposables += chipsReducer .scan(listOf()) { previousState, reducer -> reducer(previousState) } @@ -1085,8 +1068,12 @@ class ComposeViewModel @Inject constructor( // Send a message when the send button is clicked, and disable editing mode if it's enabled view.sendIntent .observeOn(Schedulers.io()) - .withLatestFrom(view.textChangedIntent) { _, body -> body.toString() } - .withLatestFrom(state, conversation, selectedChips) { body, state, conversation, chips -> + .withLatestFrom( + view.textChangedIntent, + state, + conversation, + selectedChips + ) { _, body, state, conversation, chips -> if (!permissionManager.isDefaultSms()) { view.requestDefaultSms() return@withLatestFrom @@ -1115,60 +1102,49 @@ class ComposeViewModel @Inject constructor( false -> chips.map { chip -> chip.address } } val sendAsGroup = ((addresses.size > 1) && // if more than one address to send to - (!state.editingMode || // and is not a new convo - state.sendAsGroup)) // or (is a new convo and) send as group is selected + (!state.editingMode || // and is not a new convo (group msg or not is already set) + state.sendAsGroup)) // or (is a new convo and) send as group is selected when { // Scheduling a message - state.scheduled != 0L -> { + state.scheduled != 0L -> addScheduledMessage.execute( + AddScheduledMessage.Params( + state.scheduled, + subId, + addresses, + sendAsGroup, + body.toString(), + state.attachments.map { it.uri.toString() } + ) + ).also { newState { copy(scheduled = 0) } - val uris = state.attachments.map { it.uri.toString() } - val params = AddScheduledMessage - .Params(state.scheduled, subId, addresses, sendAsGroup, body, uris) - addScheduledMessage.execute(params) context.makeToast(R.string.compose_scheduled_toast) } - // Sending a group message - sendAsGroup -> { - sendMessage.execute(SendMessage - .Params(subId, conversation.id, addresses, body, state.attachments, delay)) - } + // sending a group message + sendAsGroup -> sendMessage.execute( + SendMessage.Params( + subId, + 0, + addresses, + body.toString(), + state.attachments, + delay + ) + ) - // Sending a message to an existing conversation with one recipient - conversation.recipients.size == 1 -> { - val address = conversation.recipients.map { it.address } - sendMessage.execute(SendMessage.Params(subId, threadId, address, body, state.attachments, delay)) - } - - // Create a new conversation with one address - addresses.size == 1 -> { - sendMessage.execute(SendMessage - .Params(subId, threadId, addresses, body, state.attachments, delay)) - } - - // Send a message to multiple addresses - else -> { - addresses.forEach { - val threadId = tryOrNull(false) { - TelephonyCompat.getOrCreateThreadId(context, it) - } ?: 0 - val address = conversationRepo.getOrCreateConversation(it) - ?.recipients - ?.firstOrNull() - ?.address - ?: it - sendMessage.execute( - SendMessage.Params( - subId, - threadId, - listOf(address), - body, - state.attachments, - delay - ) + // sending message to individual address(es) + else -> addresses.forEach { + sendMessage.execute( + SendMessage.Params( + subId, + 0, + listOf(it), + body.toString(), + state.attachments, + delay ) - } + ) } } @@ -1176,12 +1152,14 @@ class ComposeViewModel @Inject constructor( // compose activity) view.clearCurrentMessageIntent.onNext( ((addresses.size > 1) && // if more than one address to send to - state.editingMode && // and is a new convo - !state.sendAsGroup) // and is *not* sent as a group - ) - } - .autoDisposable(view.scope()) - .subscribe{view.focusMessage()} + state.editingMode && // and is a new convo + !state.sendAsGroup) // and is *not* sent as a group + ) + } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { view.focusMessage() } + .autoDisposable(view.scope()) + .subscribe() // View QKSMS+ view.viewQksmsPlusIntent