Conversation as field in Message

This commit is contained in:
Moez Bhatti
2018-03-27 17:19:07 -04:00
parent dd8fd3216d
commit 9c7ff3e6ea
16 changed files with 110 additions and 130 deletions
@@ -24,6 +24,7 @@ import android.net.Uri
import android.provider.Telephony.*
import com.google.android.mms.pdu_alt.PduHeaders
import manager.KeyManager
import model.Conversation
import model.Message
import util.extensions.map
import javax.inject.Inject
@@ -77,12 +78,13 @@ class CursorToMessageImpl @Inject constructor(
id = keys.newId()
contentId = cursor.getLong(columnsMap.msgId)
threadId = cursor.getLong(columnsMap.threadId)
date = cursor.getLong(columnsMap.date)
dateSent = cursor.getLong(columnsMap.dateSent)
read = cursor.getInt(columnsMap.read) != 0
locked = cursor.getInt(columnsMap.locked) != 0
conversation = Conversation().apply { id = cursor.getLong(columnsMap.threadId) }
when (type) {
"sms" -> {
address = cursor.getString(columnsMap.smsAddress) ?: ""
@@ -34,7 +34,6 @@ import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.rxkotlin.Flowables
import io.reactivex.schedulers.Schedulers
import io.realm.Realm
import io.realm.RealmResults
@@ -64,54 +63,24 @@ class MessageRepositoryImpl @Inject constructor(
private val cursorToRecipient: CursorToRecipient,
private val prefs: Preferences) : MessageRepository {
override fun getConversations(archived: Boolean): Flowable<List<Pair<Conversation, Message>>> {
val realm = Realm.getDefaultInstance()
val conversationFlowable = realm.where(Conversation::class.java)
.equalTo("archived", archived)
.equalTo("blocked", false)
.findAllAsync()
.asFlowable()
.filter { it.isLoaded }
.map { realm.copyFromRealm(it) }
val messageFlowable = realm.where(Message::class.java)
.sort("date", Sort.DESCENDING)
override fun getConversations(archived: Boolean): RealmResults<Message> {
return Realm.getDefaultInstance()
.where(Message::class.java)
.distinctValues("threadId")
.equalTo("conversation.archived", archived)
.equalTo("conversation.blocked", false)
.sort("date", Sort.DESCENDING)
.findAllAsync()
.asFlowable()
.filter { it.isLoaded }
.map { realm.copyFromRealm(it) }
return Flowables.combineLatest(conversationFlowable, messageFlowable, { conversations, messages ->
val conversationMap = conversations.associateBy { conversation -> conversation.id }
messages.mapNotNull { message ->
val conversation = conversationMap[message.threadId]
if (conversation == null) null else Pair(conversation, message)
}
})
}
override fun getConversationsForWidget(): List<Pair<Conversation, Message>> {
override fun getConversationsForWidget(): List<Message> {
val realm = Realm.getDefaultInstance()
val conversations = realm.copyFromRealm(realm.where(Conversation::class.java)
.equalTo("archived", false)
.equalTo("blocked", false)
.findAll())
val messages = realm.copyFromRealm(realm.where(Message::class.java)
.sort("date", Sort.DESCENDING)
return realm.copyFromRealm(realm.where(Message::class.java)
.distinctValues("threadId")
.findAll())
val conversationMap = conversations.associateBy { conversation -> conversation.id }
return messages.mapNotNull { message ->
val conversation = conversationMap[message.threadId]
if (conversation == null) null else Pair(conversation, message)
}
.equalTo("conversation.archived", false)
.equalTo("conversation.blocked", false)
.sort("date", Sort.DESCENDING)
.findAllAsync())
}
override fun getBlockedConversations(): Flowable<List<Conversation>> {
@@ -383,7 +352,6 @@ class MessageRepositoryImpl @Inject constructor(
// Insert the message to Realm
val message = Message().apply {
this.threadId = threadId
this.address = address
this.body = body
this.date = System.currentTimeMillis()
@@ -393,10 +361,12 @@ class MessageRepositoryImpl @Inject constructor(
type = "sms"
read = true
seen = true
conversation = getOrCreateConversation(threadId)
}
val realm = Realm.getDefaultInstance()
var managedMessage: Message? = null
realm.executeTransaction { managedMessage = realm.copyToRealm(message) }
realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) }
// Insert the message to the native content provider
val values = ContentValues().apply {
@@ -427,14 +397,14 @@ class MessageRepositoryImpl @Inject constructor(
this.date = System.currentTimeMillis()
id = messageIds.newId()
threadId = getOrCreateConversation(address).blockingGet().id
boxId = Telephony.Sms.MESSAGE_TYPE_INBOX
type = "sms"
conversation = getOrCreateConversation(address).blockingGet()
}
val realm = Realm.getDefaultInstance()
var managedMessage: Message? = null
realm.executeTransaction { managedMessage = realm.copyToRealm(message) }
realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) }
// Insert the message to the native content provider
val values = ContentValues().apply {
@@ -97,6 +97,17 @@ class SyncRepositoryImpl @Inject constructor(
val lastSync = realm.where(Message::class.java)?.max("date")?.toLong() ?: 0
realm.insert(SyncLog())
// 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 conversations
val conversationCursor = cursorToConversation.getConversationsCursor()
val conversations = conversationCursor
@@ -112,16 +123,6 @@ class SyncRepositoryImpl @Inject constructor(
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) }
@@ -167,11 +168,11 @@ class SyncRepositoryImpl @Inject constructor(
// 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 -> existingId?.let { message.id = it } }
.doOnNext { message ->
val threadId = message.conversation?.id
message.conversation = threadId?.let { messageRepo.getOrCreateConversation(threadId) }
}
.doOnNext { message -> messageRepo.getOrCreateConversation(message.threadId) }
.doOnNext { message -> message.insertOrUpdate() }
}
@@ -36,7 +36,7 @@ class ReceiveMms @Inject constructor(
override fun buildObservable(params: Uri): Flowable<*> {
return Flowable.just(params)
.flatMap { uri -> syncManager.syncMessage(uri) } // Sync the message
.mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation
.mapNotNull { message -> message.conversation } // Map message to conversation
.filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations
.doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary
.doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification
@@ -43,7 +43,7 @@ class ReceiveSms @Inject constructor(
messageRepo.insertReceivedSms(address, body, time) // Add the message to the db
}
.mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation
.mapNotNull { message -> message.conversation } // Map message to conversation
.filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations
.doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary
.doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification
@@ -19,6 +19,7 @@
package interactor
import io.reactivex.Flowable
import model.Conversation
import model.Message
import repository.MessageRepository
import javax.inject.Inject
@@ -32,7 +33,7 @@ class RetrySending @Inject constructor(private val messageRepo: MessageRepositor
val message = Message().apply {
id = params.id
type = params.type
threadId = params.threadId
conversation = params.conversation?.id?.let { threadId -> Conversation().apply { id = threadId } }
address = params.address
body = params.body
}
+15 -1
View File
@@ -32,12 +32,26 @@ open class Message : RealmObject() {
@PrimaryKey var id: Long = 0
var conversation: Conversation? = null
set(value) {
field = value
threadId = value?.id ?: 0
}
/**
* This is a hack so that we can index by the threadId, and return distinct threadIds when
* querying for all conversations for the MainActivity
*/
@Index
@Deprecated("This is only here for Realm to access - it should be ignored in our codebase")
var threadId: Long = 0
get() = conversation?.id ?: 0
// The MMS-SMS content provider returns messages where duplicate ids can exist. This is because
// SMS and MMS are stored in separate tables. We can't use these ids as our realm message id
// since it's our primary key for the single message object, so we'll store the original id in
// case we need to access the original message item in the content provider
var contentId: Long = 0
@Index var threadId: Long = 0
var address: String = ""
var boxId: Int = 0
var type: String = ""
@@ -28,9 +28,9 @@ import model.MmsPart
interface MessageRepository {
fun getConversations(archived: Boolean = false): Flowable<List<Pair<Conversation, Message>>>
fun getConversations(archived: Boolean = false): RealmResults<Message>
fun getConversationsForWidget(): List<Pair<Conversation, Message>>
fun getConversationsForWidget(): List<Message>
fun getBlockedConversations(): Flowable<List<Conversation>>
@@ -138,8 +138,8 @@ class NotificationManagerImpl @Inject constructor(
return
}
val threadId = message.threadId
val conversation = messageRepo.getConversation(threadId) ?: return
val conversation = message.conversation ?: return
val threadId = conversation.id
val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId)
val taskStackBuilder = TaskStackBuilder.create(context)
@@ -18,13 +18,13 @@
*/
package common.util.filter
import model.Conversation
import model.Message
import javax.inject.Inject
class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter<Conversation>() {
class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter<Message>() {
override fun filter(item: Conversation, query: CharSequence): Boolean {
return item.recipients.any { recipient -> recipientFilter.filter(recipient, query) }
override fun filter(item: Message, query: CharSequence): Boolean {
return item.conversation?.recipients?.any { recipient -> recipientFilter.filter(recipient, query) } ?: false
}
}
@@ -25,20 +25,19 @@ import android.view.ViewGroup
import com.jakewharton.rxbinding2.view.clicks
import com.jakewharton.rxbinding2.view.longClicks
import com.moez.QKSMS.R
import common.Navigator
import common.base.QkViewHolder
import common.util.Colors
import common.util.DateFormatter
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import io.realm.RealmRecyclerViewAdapter
import kotlinx.android.synthetic.main.conversation_list_item.view.*
import model.Contact
import model.Conversation
import model.Message
import model.PhoneNumber
import common.Navigator
import common.base.FlowableAdapter
import common.base.QkViewHolder
import repository.MessageRepository
import javax.inject.Inject
@@ -48,7 +47,7 @@ class ConversationsAdapter @Inject constructor(
val messageRepo: MessageRepository,
val dateFormatter: DateFormatter,
val colors: Colors
) : FlowableAdapter<Pair<Conversation, Message>>() {
) : RealmRecyclerViewAdapter<Message, QkViewHolder>(null, true) {
val clicks: Subject<Long> = PublishSubject.create()
val longClicks: Subject<Long> = PublishSubject.create()
@@ -78,8 +77,8 @@ class ConversationsAdapter @Inject constructor(
}
override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) {
val conversation = getItem(position).first
val message = getItem(position).second
val message = getItem(position)!!
val conversation = message.conversation!!
val view = viewHolder.itemView
view.clicks().subscribe { clicks.onNext(conversation.id) }
@@ -94,16 +93,6 @@ class ConversationsAdapter @Inject constructor(
}
override fun getItemViewType(position: Int): Int {
return if (getItem(position).second.read) 0 else 1
}
override fun areItemsTheSame(old: Pair<Conversation, Message>, new: Pair<Conversation, Message>): Boolean {
return old.first.id == new.first.id
}
override fun areContentsTheSame(old: Pair<Conversation, Message>, new: Pair<Conversation, Message>): Boolean {
return old.second.id == new.second.id &&
old.second.read == new.second.read &&
old.first.recipients == new.first.recipients
return if (getItem(position)!!.read) 0 else 1
}
}
@@ -23,13 +23,13 @@ import android.content.Intent
import com.moez.QKSMS.R
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.base.QkViewModel
import common.util.extensions.makeToast
import injection.appComponent
import interactor.SaveImage
import io.reactivex.Flowable
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.withLatestFrom
import common.base.QkViewModel
import repository.MessageRepository
import util.extensions.mapNotNull
import javax.inject.Inject
@@ -54,7 +54,7 @@ class GalleryViewModel(intent: Intent) : QkViewModel<GalleryView, GalleryState>(
disposables += partIdFlowable
.mapNotNull { partId -> messageRepo.getMessageForPart(partId) }
.mapNotNull { message -> messageRepo.getConversation(message.threadId) }
.mapNotNull { message -> message.conversation }
.subscribe { conversation -> newState { it.copy(title = conversation.getTitle()) } }
}
@@ -199,7 +199,7 @@ class MainActivity : QkThemedActivity<MainViewModel>(), MainView {
is Inbox -> {
if (!inbox.isSelected) toolbarSearch.text = null
if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter
conversationsAdapter.flowable = state.page.data
conversationsAdapter.updateData(state.page.data)
itemTouchHelper.attachToRecyclerView(recyclerView)
menuItemAdapter.data = state.page.menu
empty.setText(R.string.inbox_empty_text)
@@ -210,7 +210,7 @@ class MainActivity : QkThemedActivity<MainViewModel>(), MainView {
is Archived -> {
if (!archived.isSelected) toolbarSearch.setText(R.string.title_archived)
if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter
conversationsAdapter.flowable = state.page.data
conversationsAdapter.updateData(state.page.data)
itemTouchHelper.attachToRecyclerView(null)
menuItemAdapter.data = state.page.menu
empty.setText(R.string.archived_empty_text)
@@ -18,10 +18,9 @@
*/
package feature.main
import io.reactivex.Flowable
import model.Conversation
import model.Message
import common.MenuItem
import io.realm.RealmResults
import model.Message
data class MainState(
val page: MainPage = Inbox(),
@@ -33,13 +32,13 @@ sealed class MainPage
data class Inbox(
val showClearButton: Boolean = false,
val data: Flowable<List<Pair<Conversation, Message>>>? = null,
val data: RealmResults<Message>? = null,
val empty: Boolean = false,
val menu: List<MenuItem> = ArrayList(),
val showArchivedSnackbar: Boolean = false) : MainPage()
data class Archived(
val data: Flowable<List<Pair<Conversation, Message>>>?,
val data: RealmResults<Message>? = null,
val empty: Boolean = false,
val menu: List<MenuItem> = ArrayList()) : MainPage()
@@ -26,8 +26,11 @@ import android.support.v4.content.ContextCompat
import com.moez.QKSMS.R
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.MenuItem
import common.Navigator
import common.base.QkViewModel
import common.util.filter.ConversationFilter
import injection.appComponent
import interactor.DeleteConversation
import interactor.MarkAllSeen
import interactor.MarkArchived
@@ -39,11 +42,10 @@ import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.withLatestFrom
import io.realm.Realm
import model.SyncLog
import common.MenuItem
import common.Navigator
import common.base.QkViewModel
import repository.MessageRepository
import util.Preferences
import util.extensions.asObservable
import util.extensions.mapNotNull
import util.extensions.toFlowable
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -63,23 +65,27 @@ class MainViewModel : QkViewModel<MainView, MainState>(MainState()) {
@Inject lateinit var prefs: Preferences
private val conversations by lazy {
messageRepo.getConversations()
.withLatestFrom(state.toFlowable(), { conversations, state ->
(state.page as? Inbox)?.let { page ->
newState { it.copy(page = page.copy(empty = conversations.isEmpty())) }
}
conversations
})
messageRepo.getConversations().also {
it.asFlowable()
.withLatestFrom(state.toFlowable(), { conversations, state ->
(state.page as? Inbox)?.let { page ->
newState { it.copy(page = page.copy(empty = conversations.isEmpty())) }
}
conversations
})
}
}
private val archivedConversations by lazy {
messageRepo.getConversations(true)
.withLatestFrom(state.toFlowable(), { conversations, state ->
(state.page as? Archived)?.let { page ->
newState { it.copy(page = page.copy(empty = conversations.isEmpty())) }
}
conversations
})
messageRepo.getConversations(true).also {
it.asFlowable()
.withLatestFrom(state.toFlowable(), { conversations, state ->
(state.page as? Archived)?.let { page ->
newState { it.copy(page = page.copy(empty = conversations.isEmpty())) }
}
conversations
})
}
}
private val menuArchive by lazy { MenuItem(context.getString(R.string.menu_archive), 0) }
@@ -137,11 +143,11 @@ class MainViewModel : QkViewModel<MainView, MainState>(MainState()) {
if (state.page is Inbox) {
val conversations = when {
query.isEmpty() -> conversations
else -> conversations.map { list -> list.filter { conversationFilter.filter(it.first, query) } }
else -> conversations.filter { conversationFilter.filter(it, query) }
}
val page = state.page.copy(showClearButton = query.isNotEmpty(), data = conversations)
newState { it.copy(page = page) }
//val page = state.page.copy(showClearButton = query.isNotEmpty(), data = conversations)
//newState { it.copy(page = page) }
}
})
.autoDisposable(view.scope())
@@ -230,9 +236,8 @@ class MainViewModel : QkViewModel<MainView, MainState>(MainState()) {
.subscribe { threadId -> deleteConversation.execute(threadId) }
val swipedConversation = view.swipeConversationIntent
.withLatestFrom(conversations.toObservable(), { position, conversations -> conversations[position] })
.map { pair -> pair.first }
.map { conversation -> conversation.id }
.withLatestFrom(conversations.asObservable(), { position, conversations -> conversations[position] })
.mapNotNull { message -> message?.conversation?.id }
swipedConversation
.withLatestFrom(state, { threadId, state ->
@@ -38,7 +38,6 @@ import common.util.extensions.dpToPx
import feature.main.MainActivity
import injection.appComponent
import model.Contact
import model.Conversation
import model.Message
import model.PhoneNumber
import repository.MessageRepository
@@ -57,7 +56,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory {
private val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
private val smallWidget = intent.getBooleanExtra("small_widget", false)
private var conversations: List<Pair<Conversation, Message>> = listOf()
private var conversations: List<Message> = listOf()
private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) }
// Cache colors, load lazily. This entire class is recreated when the widget is recreated,
@@ -98,8 +97,8 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory {
}
private fun getConversationView(position: Int): RemoteViews {
val conversation = conversations[position].first
val message = conversations[position].second
val message = conversations[position]
val conversation = message.conversation!!
val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item)