From 8f67de736a582c8f1134ba471377618affee5bf7 Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Sun, 27 Oct 2019 17:50:27 -0400 Subject: [PATCH] Sync starred contacts and groups --- .../moez/QKSMS/extensions/CursorExtensions.kt | 37 +++--------- .../QKSMS/mapper/CursorToContactGroupImpl.kt | 52 +++++++++++++++++ .../mapper/CursorToContactGroupMemberImpl.kt | 52 +++++++++++++++++ .../moez/QKSMS/mapper/CursorToContactImpl.kt | 7 ++- .../moez/QKSMS/migration/QkRealmMigration.kt | 14 ++++- .../QKSMS/repository/SyncRepositoryImpl.kt | 58 +++++++++++++------ .../moez/QKSMS/mapper/CursorToContactGroup.kt | 28 +++++++++ .../mapper/CursorToContactGroupMember.kt | 29 ++++++++++ .../main/java/com/moez/QKSMS/model/Contact.kt | 1 + .../java/com/moez/QKSMS/model/ContactGroup.kt | 29 ++++++++++ .../com/moez/QKSMS/injection/AppModule.kt | 10 ++++ 11 files changed, 267 insertions(+), 50 deletions(-) create mode 100644 data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt create mode 100644 data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt create mode 100644 domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt diff --git a/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt b/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt index 1214d0be..a23e95b2 100644 --- a/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt +++ b/data/src/main/java/com/moez/QKSMS/extensions/CursorExtensions.kt @@ -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 Cursor.map(map: (Cursor) -> T): List { } } -fun Cursor.mapWhile(map: (Cursor) -> T, predicate: (T) -> Boolean): ArrayList { - val result = ArrayList() - - 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 { .doOnComplete { close() } } -fun Cursor.asMaybe(): Maybe { - val subject = MaybeSubject.create() +/** + * Dumps the contents of the cursor as a CSV string + */ +fun Cursor.dump(): String { + val lines = mutableListOf() - 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") } - - - diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt new file mode 100644 index 00000000..f2f290b5 --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupImpl.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 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 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) + } + +} diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt new file mode 100644 index 00000000..7eb72aab --- /dev/null +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMemberImpl.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 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 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) + } + +} diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt index 82d50ca9..072e777e 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt @@ -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( } } -} \ No newline at end of file +} diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 0d5ea817..1461fa9b 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -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" } } diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 46bc532d..679f592d 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -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() } -} \ No newline at end of file + private fun getContactGroups(contacts: List): List { + 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 + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt new file mode 100644 index 00000000..2de28c6f --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroup.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 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 com.moez.QKSMS.mapper + +import android.database.Cursor +import com.moez.QKSMS.model.ContactGroup + +interface CursorToContactGroup : Mapper { + + fun getContactGroupsCursor(): Cursor? + +} diff --git a/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt new file mode 100644 index 00000000..22861fdd --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/mapper/CursorToContactGroupMember.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 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 com.moez.QKSMS.mapper + +import android.database.Cursor + +interface CursorToContactGroupMember : Mapper { + + data class GroupMember(val lookupKey: String, val groupId: Long) + + fun getGroupMembersCursor(): Cursor? + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt index e7e25637..dd325dac 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt @@ -26,5 +26,6 @@ open class Contact( @PrimaryKey var lookupKey: String = "", var numbers: RealmList = RealmList(), var name: String = "", + var starred: Boolean = false, var lastUpdate: Long = 0 ) : RealmObject() \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt b/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt new file mode 100644 index 00000000..fd0e4363 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/model/ContactGroup.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 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 com.moez.QKSMS.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +open class ContactGroup( + @PrimaryKey var id: Long = 0, + var title: String = "", + var contacts: RealmList = RealmList() +) : RealmObject() diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index 8862b189..c80d6fd1 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -51,6 +51,10 @@ import com.moez.QKSMS.manager.ShortcutManager import com.moez.QKSMS.manager.WidgetManager import com.moez.QKSMS.manager.WidgetManagerImpl import com.moez.QKSMS.mapper.CursorToContact +import com.moez.QKSMS.mapper.CursorToContactGroup +import com.moez.QKSMS.mapper.CursorToContactGroupImpl +import com.moez.QKSMS.mapper.CursorToContactGroupMember +import com.moez.QKSMS.mapper.CursorToContactGroupMemberImpl import com.moez.QKSMS.mapper.CursorToContactImpl import com.moez.QKSMS.mapper.CursorToConversation import com.moez.QKSMS.mapper.CursorToConversationImpl @@ -158,6 +162,12 @@ class AppModule(private var application: Application) { @Provides fun provideCursorToContact(mapper: CursorToContactImpl): CursorToContact = mapper + @Provides + fun provideCursorToContactGroup(mapper: CursorToContactGroupImpl): CursorToContactGroup = mapper + + @Provides + fun provideCursorToContactGroupMember(mapper: CursorToContactGroupMemberImpl): CursorToContactGroupMember = mapper + @Provides fun provideCursorToConversation(mapper: CursorToConversationImpl): CursorToConversation = mapper