Remove link between messages and conversations

This commit is contained in:
Moez Bhatti
2018-03-29 00:35:27 -04:00
parent d26adad7bf
commit ca548d97fb
19 changed files with 112 additions and 96 deletions
@@ -33,20 +33,29 @@ class CursorToConversationImpl @Inject constructor(private val context: Context)
val URI: Uri = Uri.parse("content://mms-sms/conversations?simple=true")
val PROJECTION = arrayOf(
Threads._ID,
Threads.RECIPIENT_IDS)
Threads.DATE,
Threads.RECIPIENT_IDS,
Threads.READ,
Threads.SNIPPET)
val ID = 0
val RECIPIENT_IDS = 1
const val ID = 0
const val DATE = 1
const val RECIPIENT_IDS = 2
const val READ = 3
const val SNIPPET = 4
}
override fun map(from: Cursor): Conversation {
return Conversation().apply {
id = from.getLong(ID)
date = from.getLong(DATE)
recipients.addAll(from.getString(RECIPIENT_IDS)
.split(" ")
.filter { it.isNotBlank() }
.map { recipientId -> recipientId.toLong() }
.map { recipientId -> Recipient().apply { id = recipientId } })
read = from.getInt(READ) == 1
snippet = from.getString(SNIPPET) ?: ""
}
}
@@ -24,7 +24,6 @@ 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,14 +76,13 @@ class CursorToMessageImpl @Inject constructor(
}
id = keys.newId()
threadId = cursor.getLong(columnsMap.threadId)
contentId = cursor.getLong(columnsMap.msgId)
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) ?: ""
@@ -63,26 +63,24 @@ class MessageRepositoryImpl @Inject constructor(
private val cursorToRecipient: CursorToRecipient,
private val prefs: Preferences) : MessageRepository {
override fun getConversations(archived: Boolean): RealmResults<Message> {
override fun getConversations(archived: Boolean): RealmResults<Conversation> {
return Realm.getDefaultInstance()
.where(Message::class.java)
.distinctValues("threadId")
.notEqualTo("threadId", 0L)
.equalTo("conversation.archived", archived)
.equalTo("conversation.blocked", false)
.isNotEmpty("conversation.recipients")
.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", archived)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.sort("date", Sort.DESCENDING)
.findAllAsync()
}
override fun getConversationsSnapshot(): List<Message> {
override fun getConversationsSnapshot(): List<Conversation> {
val realm = Realm.getDefaultInstance()
return realm.copyFromRealm(realm.where(Message::class.java)
.distinctValues("threadId")
.notEqualTo("threadId", 0L)
.equalTo("conversation.archived", false)
.equalTo("conversation.blocked", false)
.isNotEmpty("conversation.recipients")
return realm.copyFromRealm(realm.where(Conversation::class.java)
.notEqualTo("id", 0L)
.equalTo("archived", false)
.equalTo("blocked", false)
.isNotEmpty("recipients")
.sort("date", Sort.DESCENDING)
.findAll())
}
@@ -186,9 +184,9 @@ class MessageRepositoryImpl @Inject constructor(
.findFirst()
}
override fun getUnreadMessageCount(): Long {
override fun getUnreadCount(): Long {
return Realm.getDefaultInstance()
.where(Message::class.java)
.where(Conversation::class.java)
.equalTo("read", false)
.count()
}
@@ -234,6 +232,30 @@ class MessageRepositoryImpl @Inject constructor(
.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 message = realm
.where(Message::class.java)
.equalTo("threadId", threadId)
.sort("date", Sort.DESCENDING)
.findAll().first() ?: return
realm.executeTransaction {
conversation.date = message.date
conversation.snippet = message.getSummary()
conversation.read = message.read
conversation.me = message.isMe()
}
}
}
override fun markArchived(threadId: Long) {
val realm = Realm.getDefaultInstance()
val conversation = realm.where(Conversation::class.java)
@@ -365,6 +387,7 @@ 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()
@@ -374,8 +397,6 @@ class MessageRepositoryImpl @Inject constructor(
type = "sms"
read = true
seen = true
conversation = getOrCreateConversation(threadId)
}
val realm = Realm.getDefaultInstance()
var managedMessage: Message? = null
@@ -410,10 +431,9 @@ 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
@@ -169,10 +169,7 @@ class SyncRepositoryImpl @Inject constructor(
return cursor.asFlowable()
.map { cursorToMessage.map(Pair(it, columnsMap)) }
.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() }
}
@@ -23,13 +23,18 @@ import repository.MessageRepository
import javax.inject.Inject
class DeleteMessage @Inject constructor(
private val messageRepository: MessageRepository,
private val messageRepo: MessageRepository,
private val updateBadge: UpdateBadge
) : Interactor<Long>() {
override fun buildObservable(params: Long): Flowable<*> {
return Flowable.just(params)
.doOnNext { messageId -> messageRepository.deleteMessage(messageId) }
.doOnNext { messageId ->
val threadId = messageRepo.getMessage(messageId)?.threadId
messageRepo.deleteMessage(messageId)
threadId?.let { messageRepo.updateConversation(it) }
}
.doOnNext { messageRepo.updateConversation(params) } // Update the conversation
.flatMap { updateBadge.buildObservable(Unit) } // Update the badge
}
@@ -32,6 +32,7 @@ class MarkRead @Inject constructor(
override fun buildObservable(params: Long): Flowable<*> {
return Flowable.just(Unit)
.doOnNext { messageRepo.markRead(params) }
.doOnNext { messageRepo.updateConversation(params) } // Update the conversation
.doOnNext { notificationManager.update(params) }
.flatMap { updateBadge.buildObservable(Unit) } // Update the badge
}
@@ -24,6 +24,7 @@ import manager.NotificationManager
import repository.MessageRepository
import repository.SyncRepository
import util.extensions.mapNotNull
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class ReceiveMms @Inject constructor(
@@ -36,10 +37,13 @@ class ReceiveMms @Inject constructor(
override fun buildObservable(params: Uri): Flowable<*> {
return Flowable.just(params)
.flatMap { uri -> syncManager.syncMessage(uri) } // Sync the message
.mapNotNull { message -> message.conversation } // Map message to conversation
.doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation
.mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // 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
.map { conversation -> conversation.id } // Map to the id because [delay] will put us on the wrong thread
.delay(1, TimeUnit.SECONDS) // Wait one second before trying to notify, in case the foreground app marks it as read first
.doOnNext { threadId -> notificationManager.update(threadId) } // Update the notification
.flatMap { updateBadge.buildObservable(Unit) } // Update the badge
}
@@ -44,11 +44,13 @@ class ReceiveSms @Inject constructor(
messageRepo.insertReceivedSms(address, body, time) // Add the message to the db
}
.mapNotNull { message -> message.conversation } // Map message to conversation
.doOnNext { message -> messageRepo.updateConversation(message.threadId) } // Update the conversation
.mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // 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
.map { conversation -> conversation.id } // Map to the id because [delay] will put us on the wrong thread
.delay(1, TimeUnit.SECONDS) // Wait one second before trying to notify, in case the foreground app marks it as read first
.doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification
.doOnNext { threadId -> notificationManager.update(threadId) } // Update the notification
.flatMap { updateBadge.buildObservable(Unit) } // Update the badge
}
@@ -30,7 +30,7 @@ class UpdateBadge @Inject constructor(
override fun buildObservable(params: Unit): Flowable<*> {
return Flowable.just(params)
.map { messageRepo.getUnreadMessageCount() }
.map { messageRepo.getUnreadCount() }
.map { count -> count.toInt() }
.doOnNext { count -> widgetManager.updateUnreadCount(count) }
}
+5 -1
View File
@@ -28,9 +28,13 @@ import java.util.*
open class Conversation : RealmObject() {
@PrimaryKey var id: Long = 0
var recipients: RealmList<Recipient> = RealmList()
@Index var archived: Boolean = false
@Index var blocked: Boolean = false
var recipients: RealmList<Recipient> = RealmList()
var date: Long = 0
var snippet: String = ""
var read: Boolean = true
var me: Boolean = false
var draft: String = ""
fun getTitle() = recipients.joinToString { recipient ->
+2 -15
View File
@@ -32,19 +32,7 @@ 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
@Index var threadId: Long = 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
@@ -136,8 +124,7 @@ open class Message : RealmObject() {
.mapNotNull { it.text }
.joinToString("\n") { text -> text }
.takeIf { it.isNotEmpty() }
?: getCleansedSubject()
.takeIf { it.isNotEmpty() }
?: getCleansedSubject().takeIf { it.isNotEmpty() }
?: "An MMS message"
}
}
@@ -28,9 +28,9 @@ import model.MmsPart
interface MessageRepository {
fun getConversations(archived: Boolean = false): RealmResults<Message>
fun getConversations(archived: Boolean = false): RealmResults<Conversation>
fun getConversationsSnapshot(): List<Message>
fun getConversationsSnapshot(): List<Conversation>
fun getBlockedConversations(): Flowable<List<Conversation>>
@@ -52,7 +52,7 @@ interface MessageRepository {
fun getMessageForPart(id: Long): Message?
fun getUnreadMessageCount(): Long
fun getUnreadCount(): Long
fun getPart(id: Long): MmsPart?
@@ -70,6 +70,11 @@ interface MessageRepository {
*/
fun getUnreadMessages(threadId: Long): RealmResults<Message>
/**
* Updates message-related fields in the conversation, like the date and snippet
*/
fun updateConversation(threadId: Long)
fun markArchived(threadId: Long)
fun markUnarchived(threadId: Long)
@@ -154,7 +154,7 @@ class NotificationManagerImpl @Inject constructor(
return
}
val conversation = message.conversation ?: return
val conversation = messageRepo.getConversation(message.threadId) ?: return
val threadId = conversation.id
val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId)
@@ -18,13 +18,13 @@
*/
package common.util.filter
import model.Message
import model.Conversation
import javax.inject.Inject
class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter<Message>() {
class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter<Conversation>() {
override fun filter(item: Message, query: CharSequence): Boolean {
return item.conversation?.recipients?.any { recipient -> recipientFilter.filter(recipient, query) } ?: false
override fun filter(item: Conversation, query: CharSequence): Boolean {
return item.recipients.any { recipient -> recipientFilter.filter(recipient, query) }
}
}
@@ -19,7 +19,6 @@
package feature.conversations
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import com.jakewharton.rxbinding2.view.clicks
@@ -35,7 +34,7 @@ import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.conversation_list_item.view.*
import model.Contact
import model.Message
import model.Conversation
import model.PhoneNumber
import javax.inject.Inject
@@ -43,13 +42,12 @@ class ConversationsAdapter @Inject constructor(
private val context: Context,
private val dateFormatter: DateFormatter,
private val colors: Colors
) : QkRealmAdapter<Message>() {
) : QkRealmAdapter<Conversation>() {
val clicks: Subject<Long> = PublishSubject.create()
val longClicks: Subject<Long> = PublishSubject.create()
private val disposables = CompositeDisposable()
private var attachedRecyclerViews = 0
init {
setHasStableIds(true)
@@ -72,22 +70,8 @@ class ConversationsAdapter @Inject constructor(
return QkViewHolder(view)
}
fun isAttachedToRecyclerView() = attachedRecyclerViews > 0
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
attachedRecyclerViews++
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
attachedRecyclerViews--
disposables.clear()
}
override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) {
val message = getItem(position)!!
val conversation = message.conversation!!
val conversation = getItem(position)!!
val view = viewHolder.itemView
view.clicks().subscribe { clicks.onNext(conversation.id) }
@@ -97,12 +81,12 @@ class ConversationsAdapter @Inject constructor(
recipient.contact ?: Contact().apply { numbers.add(PhoneNumber().apply { address = recipient.address }) }
}
view.title.text = conversation.getTitle()
view.date.text = dateFormatter.getConversationTimestamp(message.date)
view.snippet.text = if (message.isMe()) "You: ${message.getSummary()}" else message.getSummary()
view.date.text = dateFormatter.getConversationTimestamp(conversation.date)
view.snippet.text = if (conversation.me) "You: ${conversation.snippet}" else conversation.snippet
}
override fun getItemId(index: Int): Long {
return getItem(index)!!.conversation!!.id
return getItem(index)!!.id
}
override fun getItemViewType(position: Int): Int {
@@ -54,7 +54,7 @@ class GalleryViewModel(intent: Intent) : QkViewModel<GalleryView, GalleryState>(
disposables += partIdFlowable
.mapNotNull { partId -> messageRepo.getMessageForPart(partId) }
.mapNotNull { message -> message.conversation }
.mapNotNull { message -> messageRepo.getConversation(message.threadId) }
.subscribe { conversation -> newState { it.copy(title = conversation.getTitle()) } }
}
@@ -20,7 +20,7 @@ package feature.main
import common.MenuItem
import io.realm.RealmResults
import model.Message
import model.Conversation
data class MainState(
val page: MainPage = Inbox(),
@@ -32,12 +32,12 @@ sealed class MainPage
data class Inbox(
val showClearButton: Boolean = false,
val data: RealmResults<Message>? = null,
val data: RealmResults<Conversation>? = null,
val menu: List<MenuItem> = ArrayList(),
val showArchivedSnackbar: Boolean = false) : MainPage()
data class Archived(
val data: RealmResults<Message>? = null,
val data: RealmResults<Conversation>? = null,
val menu: List<MenuItem> = ArrayList()) : MainPage()
data class Scheduled(
@@ -38,7 +38,7 @@ import common.util.extensions.dpToPx
import feature.main.MainActivity
import injection.appComponent
import model.Contact
import model.Message
import model.Conversation
import model.PhoneNumber
import repository.MessageRepository
import javax.inject.Inject
@@ -56,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<Message> = listOf()
private var conversations: List<Conversation> = listOf()
private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) }
// Cache colors, load lazily. This entire class is recreated when the widget is recreated,
@@ -97,8 +97,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory {
}
private fun getConversationView(position: Int): RemoteViews {
val message = conversations[position]
val conversation = message.conversation!!
val conversation = conversations[position]
val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item)
@@ -138,15 +137,15 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory {
// Name
remoteViews.setTextColor(R.id.name, textPrimary)
remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), !message.read))
remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), !conversation.read))
// Date
remoteViews.setTextColor(R.id.date, if (message.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.date, boldText(dateFormatter.getConversationTimestamp(message.date), !message.read))
remoteViews.setTextColor(R.id.date, if (conversation.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.date, boldText(dateFormatter.getConversationTimestamp(conversation.date), !conversation.read))
// Snippet
remoteViews.setTextColor(R.id.snippet, if (message.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.snippet, boldText(message.getSummary(), !message.read))
remoteViews.setTextColor(R.id.snippet, if (conversation.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.snippet, boldText(conversation.snippet, !conversation.read))
// Launch conversation on click
val clickIntent = Intent().putExtra("threadId", conversation.id)
@@ -49,6 +49,7 @@ class SendMessage @Inject constructor(
sendMms(params.threadId, params.addresses, params.body, params.attachments)
}
}
.doOnNext { messageRepo.updateConversation(params.threadId) }
.doOnNext { messageRepo.markUnarchived(params.threadId) }
}