Files
quik/data/src/main/java/repository/SyncRepositoryImpl.kt
T
2018-03-26 01:49:21 -04:00

213 lines
7.4 KiB
Kotlin

/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package repository
import android.content.ContentResolver
import android.content.ContentUris
import android.net.Uri
import android.provider.Telephony
import android.telephony.PhoneNumberUtils
import io.reactivex.Flowable
import io.realm.Realm
import mapper.CursorToContact
import mapper.CursorToConversation
import mapper.CursorToMessage
import mapper.CursorToRecipient
import model.Contact
import model.Conversation
import model.Message
import model.MmsPart
import model.Recipient
import model.SyncLog
import util.extensions.asFlowable
import util.extensions.insertOrUpdate
import util.extensions.map
import util.extensions.mapWhile
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SyncRepositoryImpl @Inject constructor(
private val contentResolver: ContentResolver,
private val messageRepo: MessageRepository,
private val cursorToConversation: CursorToConversation,
private val cursorToMessage: CursorToMessage,
private val cursorToRecipient: CursorToRecipient,
private val cursorToContact: CursorToContact
) : SyncRepository {
sealed class Status {
class Idle : Status()
class Running : Status()
}
/**
* Holds data that should be persisted across full syncs
*/
private data class PersistedData(val id: Long, val archived: Boolean, val blocked: Boolean)
private var status: Status = Status.Idle()
override fun syncMessages(fullSync: Boolean) {
// If the sync is already running, don't try to do another one
if (status is Status.Running) return
status = Status.Running()
val realm = Realm.getDefaultInstance()
realm.beginTransaction()
var persistedData: List<PersistedData>? = null
if (fullSync) {
persistedData = realm.where(Conversation::class.java)
.beginGroup()
.equalTo("archived", true)
.or()
.equalTo("blocked", true)
.endGroup()
.findAll()
.map { conversation ->
PersistedData(conversation.id, conversation.archived, conversation.blocked)
}
realm.delete(Conversation::class.java)
realm.delete(Message::class.java)
realm.delete(MmsPart::class.java)
realm.delete(Recipient::class.java)
realm.delete(SyncLog::class.java)
}
val lastSync = realm.where(Message::class.java)?.max("date")?.toLong() ?: 0
realm.insert(SyncLog())
// Sync conversations
val conversationCursor = cursorToConversation.getConversationsCursor()
val conversations = conversationCursor
.map { cursor -> cursorToConversation.map(cursor) }
persistedData?.forEach { data ->
val conversation = conversations.firstOrNull { conversation -> conversation.id == data.id }
conversation?.archived = data.archived
conversation?.blocked = data.blocked
}
realm.insertOrUpdate(conversations)
conversationCursor.close()
// Sync messages
val messageCursor = cursorToMessage.getMessagesCursor()
val messageColumns = CursorToMessage.MessageColumns(messageCursor)
val messages = messageCursor.mapWhile(
{ cursor -> cursorToMessage.map(Pair(cursor, messageColumns)) },
{ message -> message.date > lastSync })
realm.insertOrUpdate(messages)
messageCursor.close()
// Sync recipients
val recipientCursor = cursorToRecipient.getRecipientCursor()
val recipients = recipientCursor.map { cursor -> cursorToRecipient.map(cursor) }
realm.insertOrUpdate(recipients)
recipientCursor.close()
realm.commitTransaction()
realm.close()
syncContacts()
status = Status.Idle()
}
override fun syncMessage(uri: Uri): Flowable<Message> {
val id = ContentUris.parseId(uri)
val type = when {
uri.toString().contains("mms") -> "mms"
uri.toString().contains("sms") -> "sms"
else -> return Flowable.empty()
}
// Check if the message already exists, so we can reuse the id
val existingId = Realm.getDefaultInstance().use { realm ->
realm.refresh()
realm.where(Message::class.java)
.equalTo("type", type)
.equalTo("contentId", id)
.findFirst()
?.id
}
// The uri might be something like content://mms/inbox/id
// The box might change though, so we should just use the mms/id uri
val stableUri = when (type) {
"mms" -> ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI, id)
else -> ContentUris.withAppendedId(Telephony.Sms.CONTENT_URI, id)
}
val cursor = contentResolver.query(stableUri, null, null, null, null)
val columnsMap = CursorToMessage.MessageColumns(cursor)
// Map the cursor to a message
return cursor.asFlowable()
.map { cursorToMessage.map(Pair(it, columnsMap)) }
.map { message ->
existingId?.let { message.id = it }
message
}
.doOnNext { message -> messageRepo.getOrCreateConversation(message.threadId) }
.doOnNext { message -> message.insertOrUpdate() }
}
override fun syncContacts() {
// Load all the contacts
var contacts = cursorToContact.getContactsCursor()
.map { cursor -> cursorToContact.map(cursor) }
.groupBy { contact -> contact.lookupKey }
.map { contacts ->
val allNumbers = contacts.value.map { it.numbers }.flatten()
contacts.value.first().apply {
numbers.clear()
numbers.addAll(allNumbers)
}
}
val realm = Realm.getDefaultInstance()
val recipients = realm.where(Recipient::class.java).findAll()
realm.executeTransaction {
realm.delete(Contact::class.java)
contacts = realm.copyToRealm(contacts)
// Update all the recipients with the new contacts
val updatedRecipients = recipients.map { recipient ->
recipient.apply {
contact = contacts.firstOrNull {
it.numbers.any { PhoneNumberUtils.compare(recipient.address, it.address) }
}
}
}
realm.insertOrUpdate(updatedRecipients)
}
realm.close()
}
}