Sync starred contacts and groups

This commit is contained in:
moezbhatti
2019-10-27 17:50:27 -04:00
committed by Moez Bhatti
parent 28cad9bbc9
commit 8f67de736a
11 changed files with 267 additions and 50 deletions
@@ -20,8 +20,6 @@ package com.moez.QKSMS.extensions
import android.database.Cursor
import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.subjects.MaybeSubject
fun Cursor.forEach(closeOnComplete: Boolean = true, method: (Cursor) -> Unit = {}) {
moveToPosition(-1)
@@ -41,21 +39,6 @@ fun <T> Cursor.map(map: (Cursor) -> T): List<T> {
}
}
fun <T> Cursor.mapWhile(map: (Cursor) -> T, predicate: (T) -> Boolean): ArrayList<T> {
val result = ArrayList<T>()
moveToPosition(-1)
while (moveToNext()) {
val item = map(this)
if (!predicate(item)) break
result.add(item)
}
return result
}
/**
* We're using this simple implementation with .range() because of the
* complexities of dealing with Backpressure with a Cursor. We can't simply
@@ -72,18 +55,14 @@ fun Cursor.asFlowable(): Flowable<Cursor> {
.doOnComplete { close() }
}
fun Cursor.asMaybe(): Maybe<Cursor> {
val subject = MaybeSubject.create<Cursor>()
/**
* Dumps the contents of the cursor as a CSV string
*/
fun Cursor.dump(): String {
val lines = mutableListOf<String>()
if (moveToFirst()) {
subject.onSuccess(this)
} else {
subject.onError(IndexOutOfBoundsException("The cursor has no items"))
}
lines += columnNames.joinToString(",")
forEach { lines += (0 until columnCount).joinToString(",", transform = ::getString) }
subject.doOnComplete { close() }
return subject
return lines.joinToString("\n")
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2019 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 com.moez.QKSMS.mapper
import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract
import com.moez.QKSMS.model.ContactGroup
import javax.inject.Inject
class CursorToContactGroupImpl @Inject constructor(
private val context: Context
) : CursorToContactGroup {
companion object {
private val URI = ContactsContract.Groups.CONTENT_URI
private val PROJECTION = arrayOf(
ContactsContract.Groups._ID,
ContactsContract.Groups.TITLE)
private const val SELECTION = "${ContactsContract.Groups.AUTO_ADD}=0 " +
"AND ${ContactsContract.Groups.DELETED}=0 " +
"AND ${ContactsContract.Groups.FAVORITES}=0"
private const val ID = 0
private const val TITLE = 1
}
override fun map(from: Cursor): ContactGroup {
return ContactGroup(from.getLong(ID), from.getString(TITLE))
}
override fun getContactGroupsCursor(): Cursor? {
return context.contentResolver.query(URI, PROJECTION, SELECTION, null, null)
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2019 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 com.moez.QKSMS.mapper
import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract
import javax.inject.Inject
class CursorToContactGroupMemberImpl @Inject constructor(
private val context: Context
) : CursorToContactGroupMember {
companion object {
private val URI = ContactsContract.Data.CONTENT_URI
private val PROJECTION = arrayOf(
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.Data.DATA1)
private const val SELECTION = "${ContactsContract.Data.MIMETYPE}=?"
private val SELECTION_ARGS = arrayOf(
ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE)
private const val LOOKUP_KEY = 0
private const val GROUP_ID = 1
}
override fun map(from: Cursor): CursorToContactGroupMember.GroupMember {
return CursorToContactGroupMember.GroupMember(from.getString(LOOKUP_KEY), from.getLong(GROUP_ID))
}
override fun getGroupMembersCursor(): Cursor? {
return context.contentResolver.query(URI, PROJECTION, SELECTION, SELECTION_ARGS, null)
}
}
@@ -39,6 +39,7 @@ class CursorToContactImpl @Inject constructor(
Phone.TYPE,
Phone.LABEL,
Phone.DISPLAY_NAME,
Phone.STARRED,
Phone.CONTACT_LAST_UPDATED_TIMESTAMP
)
@@ -47,7 +48,8 @@ class CursorToContactImpl @Inject constructor(
const val COLUMN_TYPE = 2
const val COLUMN_LABEL = 3
const val COLUMN_DISPLAY_NAME = 4
const val CONTACT_LAST_UPDATED = 5
const val COLUMN_STARRED = 5
const val CONTACT_LAST_UPDATED = 6
}
override fun map(from: Cursor) = Contact().apply {
@@ -58,6 +60,7 @@ class CursorToContactImpl @Inject constructor(
type = Phone.getTypeLabel(context.resources, from.getInt(COLUMN_TYPE),
from.getString(COLUMN_LABEL)).toString()
))
starred = from.getInt(COLUMN_STARRED) != 0
lastUpdate = from.getLong(CONTACT_LAST_UPDATED)
}
@@ -68,4 +71,4 @@ class CursorToContactImpl @Inject constructor(
}
}
}
}
@@ -26,7 +26,7 @@ import io.realm.Sort
class QkRealmMigration : RealmMigration {
companion object {
const val SCHEMA_VERSION: Long = 8
const val SCHEMA_VERSION: Long = 9
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@@ -118,6 +118,18 @@ class QkRealmMigration : RealmMigration {
version++
}
if (version == 8L) {
realm.schema.create("ContactGroup")
.addField("id", Long::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED)
.addField("title", String::class.java, FieldAttribute.REQUIRED)
.addRealmListField("contacts", realm.schema.get("Contact"))
realm.schema.get("Contact")
?.addField("starred", Boolean::class.java, FieldAttribute.REQUIRED)
version++
}
check(version >= newVersion) { "Migration missing from v$oldVersion to v$newVersion" }
}
@@ -27,10 +27,13 @@ import com.moez.QKSMS.extensions.insertOrUpdate
import com.moez.QKSMS.extensions.map
import com.moez.QKSMS.manager.KeyManager
import com.moez.QKSMS.mapper.CursorToContact
import com.moez.QKSMS.mapper.CursorToContactGroup
import com.moez.QKSMS.mapper.CursorToContactGroupMember
import com.moez.QKSMS.mapper.CursorToConversation
import com.moez.QKSMS.mapper.CursorToMessage
import com.moez.QKSMS.mapper.CursorToRecipient
import com.moez.QKSMS.model.Contact
import com.moez.QKSMS.model.ContactGroup
import com.moez.QKSMS.model.Conversation
import com.moez.QKSMS.model.Message
import com.moez.QKSMS.model.MmsPart
@@ -53,6 +56,8 @@ class SyncRepositoryImpl @Inject constructor(
private val cursorToMessage: CursorToMessage,
private val cursorToRecipient: CursorToRecipient,
private val cursorToContact: CursorToContact,
private val cursorToContactGroup: CursorToContactGroup,
private val cursorToContactGroupMember: CursorToContactGroupMember,
private val keys: KeyManager,
private val phoneNumberUtils: PhoneNumberUtils,
private val rxPrefs: RxSharedPreferences
@@ -89,6 +94,7 @@ class SyncRepositoryImpl @Inject constructor(
.toMutableMap()
realm.delete(Contact::class.java)
realm.delete(ContactGroup::class.java)
realm.delete(Conversation::class.java)
realm.delete(Message::class.java)
realm.delete(MmsPart::class.java)
@@ -234,19 +240,19 @@ class SyncRepositoryImpl @Inject constructor(
realm.executeTransaction {
realm.delete(Contact::class.java)
realm.delete(ContactGroup::class.java)
contacts = realm.copyToRealm(contacts)
realm.insertOrUpdate(getContactGroups(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) }
}
recipients.forEach { recipient ->
recipient.contact = contacts.find { contact ->
contact.numbers.any { phoneNumberUtils.compare(recipient.address, it.address) }
}
}
realm.insertOrUpdate(updatedRecipients)
realm.insertOrUpdate(recipients)
}
}
@@ -254,26 +260,24 @@ class SyncRepositoryImpl @Inject constructor(
override fun syncContact(address: String): Boolean {
// See if there's a contact that matches this phone number
var contact = getContacts().firstOrNull {
it.numbers.any { number -> phoneNumberUtils.compare(number.address, address) }
var contact = getContacts().find { contact ->
contact.numbers.any { number -> phoneNumberUtils.compare(number.address, address) }
} ?: return false
Realm.getDefaultInstance().use { realm ->
val recipients = realm.where(Recipient::class.java).findAll()
val recipients = realm.where(Recipient::class.java).findAll().filter { recipient ->
contact.numbers.any { number ->
phoneNumberUtils.compare(recipient.address, number.address)
}
}
realm.executeTransaction {
contact = realm.copyToRealmOrUpdate(contact)
// Update all the matching recipients with the new contact
val updatedRecipients = recipients
.filter { recipient ->
contact.numbers.any { number ->
phoneNumberUtils.compare(recipient.address, number.address)
}
}
.map { recipient -> recipient.apply { this.contact = contact } }
recipients.forEach { recipient -> recipient.contact = contact }
realm.insertOrUpdate(updatedRecipients)
realm.insertOrUpdate(recipients)
}
}
@@ -293,4 +297,22 @@ class SyncRepositoryImpl @Inject constructor(
} ?: listOf()
}
}
private fun getContactGroups(contacts: List<Contact>): List<ContactGroup> {
val groupMembers = cursorToContactGroupMember.getGroupMembersCursor()
?.map(cursorToContactGroupMember::map)
.orEmpty()
val groups = cursorToContactGroup.getContactGroupsCursor()
?.map(cursorToContactGroup::map)
.orEmpty()
groups.forEach { group ->
group.contacts.addAll(groupMembers
.filter { member -> member.groupId == group.id }
.mapNotNull { member -> contacts.find { contact -> contact.lookupKey == member.lookupKey } })
}
return groups
}
}