/* * Copyright (C) 2017 Moez Bhatti * * This file is part of QKSMS. * * QKSMS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * QKSMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ package repository import android.app.PendingIntent import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.content.Intent import android.net.Uri import android.provider.BaseColumns import android.provider.Telephony import android.telephony.PhoneNumberUtils import android.telephony.SmsManager import com.klinker.android.send_message.BroadcastUtils import com.klinker.android.send_message.StripAccents import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import io.realm.Realm import io.realm.RealmResults import io.realm.Sort import manager.ExternalBlockingManager import manager.KeyManager import mapper.CursorToConversation import mapper.CursorToRecipient import model.Contact import model.Conversation import model.Message import model.MmsPart import util.MessageUtils import util.Preferences import util.extensions.asFlowable import util.extensions.asMaybe import util.extensions.asObservable import util.extensions.insertOrUpdate import util.extensions.map import javax.inject.Inject import javax.inject.Singleton @Singleton class MessageRepositoryImpl @Inject constructor( private val context: Context, private val messageIds: KeyManager, private val cursorToConversation: CursorToConversation, private val cursorToRecipient: CursorToRecipient, private val externalBlockingManager: ExternalBlockingManager, private val prefs: Preferences) : MessageRepository { override fun getConversations(archived: Boolean): Flowable> { val realm = Realm.getDefaultInstance() return realm .where(Conversation::class.java) .notEqualTo("id", 0L) .greaterThan("count", 0) .equalTo("archived", archived) .equalTo("blocked", false) .isNotEmpty("recipients") .sort("date", Sort.DESCENDING) .findAllAsync() .asFlowable() .filter { it.isLoaded } .filter { it.isValid } .map { conversations -> realm.copyFromRealm(conversations) } } override fun getConversationsSnapshot(): List { val realm = Realm.getDefaultInstance() return realm.copyFromRealm(realm.where(Conversation::class.java) .notEqualTo("id", 0L) .greaterThan("count", 0) .equalTo("archived", false) .equalTo("blocked", false) .isNotEmpty("recipients") .sort("date", Sort.DESCENDING) .findAll()) } override fun getBlockedConversations(): Flowable> { return Realm.getDefaultInstance() .where(Conversation::class.java) .equalTo("blocked", true) .findAllAsync() .asFlowable() .filter { it.isLoaded } .map { it.toList() } } 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() .where(Conversation::class.java) .equalTo("id", threadId) .findFirst() } override fun getOrCreateConversation(threadId: Long): Conversation? { return getConversation(threadId) ?: getConversationFromCp(threadId) } override fun getOrCreateConversation(address: String): Maybe { return getOrCreateConversation(listOf(address)) } override fun getOrCreateConversation(addresses: List): Maybe { return Maybe.just(addresses) .map { recipients -> recipients.map { address -> when (MessageUtils.isEmailAddress(address)) { true -> MessageUtils.extractAddrSpec(address) false -> address } } } .map { recipients -> Uri.parse("content://mms-sms/threadID").buildUpon().apply { recipients.forEach { recipient -> appendQueryParameter("recipient", recipient) } } } .flatMap { uriBuilder -> context.contentResolver.query(uriBuilder.build(), arrayOf(BaseColumns._ID), null, null, null).asMaybe() } .map { cursor -> cursor.getLong(0) } .filter { threadId -> threadId != 0L } .map { threadId -> var conversation = getConversation(threadId) if (conversation != null) conversation = Realm.getDefaultInstance().copyFromRealm(conversation) conversation ?: getConversationFromCp(threadId)?.apply { insertOrUpdate() } ?: Conversation() } .onErrorReturn { Conversation() } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) } override fun saveDraft(threadId: Long, draft: String) { Realm.getDefaultInstance().use { realm -> val conversation = realm.where(Conversation::class.java) .equalTo("id", threadId) .findFirst() conversation?.let { realm.executeTransaction { conversation.draft = draft } } } } override fun getMessages(threadId: Long): RealmResults { return Realm.getDefaultInstance() .where(Message::class.java) .equalTo("threadId", threadId) .sort("date") .findAllAsync() } override fun getMessage(id: Long): Message? { return Realm.getDefaultInstance() .where(Message::class.java) .equalTo("id", id) .findFirst() } override fun getMessageForPart(id: Long): Message? { return Realm.getDefaultInstance() .where(Message::class.java) .equalTo("parts.id", id) .findFirst() } override fun getUnreadCount(): Long { return Realm.getDefaultInstance() .where(Conversation::class.java) .equalTo("read", false) .count() } override fun getPart(id: Long): MmsPart? { return Realm.getDefaultInstance() .where(MmsPart::class.java) .equalTo("id", id) .findFirst() } override fun getPartsForConversation(threadId: Long): Observable> { return Realm.getDefaultInstance() .where(Message::class.java) .equalTo("threadId", threadId) .isNotEmpty("parts") .sort("date") .findAll() .asObservable() .map { messages -> messages.flatMap { it.parts } } } /** * Retrieves the list of messages which should be shown in the notification * for a given conversation */ override fun getUnreadUnseenMessages(threadId: Long): RealmResults { return Realm.getDefaultInstance() .also { it.refresh() } .where(Message::class.java) .equalTo("seen", false) .equalTo("read", false) .equalTo("threadId", threadId) .sort("date") .findAll() } override fun getUnreadMessages(threadId: Long): RealmResults { return Realm.getDefaultInstance() .where(Message::class.java) .equalTo("read", false) .equalTo("threadId", threadId) .sort("date") .findAll() } override fun updateConversation(threadId: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() val conversation = realm .where(Conversation::class.java) .equalTo("id", threadId) .findFirst() ?: return val messages = realm .where(Message::class.java) .equalTo("threadId", threadId) .sort("date", Sort.DESCENDING) .findAll() val message = messages.firstOrNull() realm.executeTransaction { conversation.count = messages.size conversation.date = message?.date ?: 0 conversation.snippet = message?.getSummary() ?: "" conversation.read = message?.read ?: true conversation.me = message?.isMe() ?: false } } } override fun markArchived(threadId: Long) { val realm = Realm.getDefaultInstance() val conversation = realm.where(Conversation::class.java) .equalTo("id", threadId) .findFirst() realm.executeTransaction { conversation?.archived = true } realm.close() } override fun markUnarchived(threadId: Long) { val realm = Realm.getDefaultInstance() val conversation = realm.where(Conversation::class.java) .equalTo("id", threadId) .findFirst() realm.executeTransaction { conversation?.archived = false } realm.close() } override fun markBlocked(threadId: Long) { val realm = Realm.getDefaultInstance() val conversation = realm.where(Conversation::class.java) .equalTo("id", threadId) .findFirst() realm.executeTransaction { conversation?.blocked = true } realm.close() } override fun markUnblocked(threadId: Long) { val realm = Realm.getDefaultInstance() val conversation = realm.where(Conversation::class.java) .equalTo("id", threadId) .findFirst() realm.executeTransaction { conversation?.blocked = false } realm.close() } override fun markAllSeen() { val realm = Realm.getDefaultInstance() val messages = realm.where(Message::class.java).equalTo("seen", false).findAll() realm.executeTransaction { messages.forEach { message -> message.seen = true } } realm.close() } override fun markSeen(threadId: Long) { val realm = Realm.getDefaultInstance() val messages = realm.where(Message::class.java) .equalTo("threadId", threadId) .equalTo("seen", false) .findAll() realm.executeTransaction { messages.forEach { message -> message.seen = true } } realm.close() } override fun markRead(threadId: Long) { // TODO also need to mark MMS in ContentProvider as Read val projection = arrayOf(BaseColumns._ID) val selection = "${Telephony.Sms.THREAD_ID} = $threadId AND (${Telephony.Sms.SEEN} = 0 OR ${Telephony.Sms.READ} = 0)" val contentResolver = context.contentResolver contentResolver.query(Telephony.Sms.Inbox.CONTENT_URI, projection, selection, null, null) .asFlowable() .subscribeOn(Schedulers.io()) .map { cursor -> cursor.getLong(0) } .map { id -> Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, id.toString()) } .subscribe { uri -> val values = ContentValues() values.put(Telephony.Sms.SEEN, true) values.put(Telephony.Sms.READ, true) contentResolver.update(uri, values, null, null) } val realm = Realm.getDefaultInstance() val messages = realm.where(Message::class.java) .equalTo("threadId", threadId) .beginGroup() .equalTo("read", false) .or() .equalTo("seen", false) .endGroup() .findAll() realm.executeTransaction { messages.forEach { message -> message.seen = true message.read = true } } realm.close() } override fun sendSmsAndPersist(threadId: Long, address: String, body: String) { val message = insertSentSms(threadId, address, body) sendSms(message) } override fun sendSms(message: Message) { val smsManager = SmsManager.getDefault() val parts = smsManager.divideMessage(if (prefs.unicode.get()) StripAccents.stripAccents(message.body) else message.body) val sentIntents = parts.map { val action = "com.moez.QKSMS.SMS_SENT" val intent = Intent(action).putExtra("id", message.id) BroadcastUtils.addClassName(context, intent, action) PendingIntent.getBroadcast(context, message.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) } val deliveredIntents = parts.map { val action = "com.moez.QKSMS.SMS_DELIVERED" val intent = Intent(action).putExtra("id", message.id) BroadcastUtils.addClassName(context, intent, action) val pendingIntent = PendingIntent.getBroadcast(context, message.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) if (prefs.delivery.get()) pendingIntent else null } smsManager.sendMultipartTextMessage(message.address, null, parts, ArrayList(sentIntents), ArrayList(deliveredIntents)) } override fun insertSentSms(threadId: Long, address: String, body: String): Message { // Insert the message to Realm val message = Message().apply { this.threadId = threadId this.address = address this.body = body this.date = System.currentTimeMillis() id = messageIds.newId() boxId = Telephony.Sms.MESSAGE_TYPE_OUTBOX type = "sms" read = true seen = true } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) } // Insert the message to the native content provider val values = ContentValues().apply { put(Telephony.Sms.ADDRESS, address) put(Telephony.Sms.BODY, body) put(Telephony.Sms.DATE, System.currentTimeMillis()) put(Telephony.Sms.READ, true) put(Telephony.Sms.SEEN, true) put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_OUTBOX) put(Telephony.Sms.THREAD_ID, threadId) } val uri = context.contentResolver.insert(Telephony.Sms.CONTENT_URI, values) // Update the contentId after the message has been inserted to the content provider realm.executeTransaction { managedMessage?.contentId = uri.lastPathSegment.toLong() } realm.close() return message } override fun insertReceivedSms(address: String, body: String, sentTime: Long): Message { // Insert the message to Realm val message = Message().apply { this.address = address this.body = body this.dateSent = sentTime this.date = System.currentTimeMillis() id = messageIds.newId() threadId = getOrCreateConversation(address).blockingGet().id boxId = Telephony.Sms.MESSAGE_TYPE_INBOX type = "sms" } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) } // Insert the message to the native content provider val values = ContentValues().apply { put(Telephony.Sms.ADDRESS, address) put(Telephony.Sms.BODY, body) put(Telephony.Sms.DATE_SENT, sentTime) } val uri = context.contentResolver.insert(Telephony.Sms.Inbox.CONTENT_URI, values) // Update the contentId after the message has been inserted to the content provider realm.executeTransaction { managedMessage?.contentId = uri.lastPathSegment.toLong() } realm.close() return message } /** * Marks the message as sending, in case we need to retry sending it */ override fun markSending(id: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() val message = realm.where(Message::class.java).equalTo("id", id).findFirst() message?.let { // Update the message in realm realm.executeTransaction { message.boxId = Telephony.Sms.MESSAGE_TYPE_OUTBOX } // Update the message in the native ContentProvider val values = ContentValues() values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_OUTBOX) context.contentResolver.update(message.getUri(), values, null, null) } } } override fun markSent(id: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() val message = realm.where(Message::class.java).equalTo("id", id).findFirst() message?.let { // Update the message in realm realm.executeTransaction { message.boxId = Telephony.Sms.MESSAGE_TYPE_SENT } // Update the message in the native ContentProvider val values = ContentValues() values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT) context.contentResolver.update(message.getUri(), values, null, null) } } } override fun markFailed(id: Long, resultCode: Int) { Realm.getDefaultInstance().use { realm -> realm.refresh() val message = realm.where(Message::class.java).equalTo("id", id).findFirst() message?.let { // Update the message in realm realm.executeTransaction { message.boxId = Telephony.Sms.MESSAGE_TYPE_FAILED message.errorCode = resultCode } // Update the message in the native ContentProvider val values = ContentValues() values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_FAILED) values.put(Telephony.Sms.ERROR_CODE, resultCode) context.contentResolver.update(message.getUri(), values, null, null) } } } override fun markDelivered(id: Long) { Realm.getDefaultInstance().use { realm -> realm.refresh() val message = realm.where(Message::class.java).equalTo("id", id).findFirst() message?.let { // Update the message in realm realm.executeTransaction { message.deliveryStatus = Telephony.Sms.STATUS_COMPLETE message.dateSent = System.currentTimeMillis() message.read = true } // Update the message in the native ContentProvider val values = ContentValues() values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_COMPLETE) values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis()) values.put(Telephony.Sms.READ, true) context.contentResolver.update(message.getUri(), values, null, null) } } } override fun markDeliveryFailed(id: Long, resultCode: Int) { Realm.getDefaultInstance().use { realm -> realm.refresh() val message = realm.where(Message::class.java).equalTo("id", id).findFirst() message?.let { // Update the message in realm realm.executeTransaction { message.deliveryStatus = Telephony.Sms.STATUS_FAILED message.dateSent = System.currentTimeMillis() message.read = true message.errorCode = resultCode } // Update the message in the native ContentProvider val values = ContentValues() values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_FAILED) values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis()) values.put(Telephony.Sms.READ, true) values.put(Telephony.Sms.ERROR_CODE, resultCode) context.contentResolver.update(message.getUri(), values, null, null) } } } override fun deleteMessage(messageId: Long) { val realm = Realm.getDefaultInstance() realm.where(Message::class.java).equalTo("id", messageId).findFirst()?.let { message -> val uri = message.getUri() realm.executeTransaction { message.deleteFromRealm() } context.contentResolver.delete(uri, null, null) } realm.close() } override fun deleteConversation(threadId: Long) { Realm.getDefaultInstance().use { realm -> val conversation = realm.where(Conversation::class.java).equalTo("id", threadId).findAll() val messages = realm.where(Message::class.java).equalTo("threadId", threadId).findAll() realm.executeTransaction { conversation.deleteAllFromRealm() messages.deleteAllFromRealm() } } val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId) context.contentResolver.delete(uri, null, null) } private fun getConversationFromCp(threadId: Long): Conversation? { var conversation: Conversation? = null val cursor = cursorToConversation.getConversationCursor(threadId) if (cursor.moveToFirst()) { conversation = cursorToConversation.map(cursor) val realm = Realm.getDefaultInstance() val contacts = realm.copyFromRealm(realm.where(Contact::class.java).findAll()) val recipients = conversation.recipients .map { recipient -> recipient.id } .map { id -> cursorToRecipient.getRecipientCursor(id) } .map { recipientCursor -> recipientCursor.map { cursorToRecipient.map(recipientCursor) } } .flatten() .map { recipient -> recipient.apply { contact = contacts.firstOrNull { it.numbers.any { PhoneNumberUtils.compare(recipient.address, it.address) } } } } // If any of the addresses here should be blocked, then block the conversation conversation.blocked = recipients.map { it.address }.any { address -> externalBlockingManager.shouldBlock(address).blockingGet() } conversation.recipients.clear() conversation.recipients.addAll(recipients) conversation.insertOrUpdate() realm.close() } cursor.close() return conversation } }