LocalContact.kt 9.6 KB
Newer Older
1
/*
Ricki Hirner's avatar
Ricki Hirner committed
2
 * Copyright © Ricki Hirner (bitfire web engineering).
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */

package at.bitfire.davdroid.resource

import android.content.ContentProviderOperation
import android.content.ContentValues
import android.os.Build
import android.os.RemoteException
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.RawContacts.Data
import at.bitfire.davdroid.BuildConfig
19
import at.bitfire.davdroid.log.Logger
20 21 22 23 24 25
import at.bitfire.davdroid.model.UnknownProperties
import at.bitfire.vcard4android.*
import ezvcard.Ezvcard
import java.io.FileNotFoundException
import java.util.*

26
class LocalContact: AndroidContact, LocalAddress {
27 28 29 30 31 32

    companion object {
        init {
            Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ez-vcard/" + Ezvcard.VERSION
        }

33 34
        const val COLUMN_FLAGS = ContactsContract.RawContacts.SYNC4
        const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
35 36 37 38 39 40

    }

    private val cachedGroupMemberships = HashSet<Long>()
    private val groupMemberships = HashSet<Long>()

41 42
    override var flags: Int = 0
        private set
43 44


45 46
    constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
            : super(addressBook, values) {
47
        flags = values.getAsInteger(COLUMN_FLAGS) ?: 0
48
    }
49

50 51 52 53
    constructor(addressBook: AndroidAddressBook<LocalContact,*>, contact: Contact, fileName: String?, eTag: String?, flags: Int)
            : super(addressBook, contact, fileName, eTag) {
        this.flags = flags
    }
54

55 56 57 58 59 60 61 62 63 64 65

    override fun assignNameAndUID() {
        val uid = UUID.randomUUID().toString()
        val newFileName = "$uid.vcf"

        val values = ContentValues(2)
        values.put(COLUMN_FILENAME, newFileName)
        values.put(COLUMN_UID, uid)
        addressBook.provider!!.update(rawContactSyncURI(), values, null, null)

        fileName = newFileName
66 67
    }

68 69 70
    override fun clearDirty(eTag: String?) {
        val values = ContentValues(3)
        values.put(COLUMN_ETAG, eTag)
71
        values.put(ContactsContract.RawContacts.DIRTY, 0)
72 73 74 75 76 77

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            // workaround for Android 7 which sets DIRTY flag when only meta-data is changed
            val hashCode = dataHashCode()
            values.put(COLUMN_HASHCODE, hashCode)
            Logger.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
78 79
        }

80
        addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
81

82 83
        this.eTag = eTag
    }
84

85 86 87 88
    override fun resetDeleted() {
        val values = ContentValues(1)
        values.put(ContactsContract.Groups.DELETED, 0)
        addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
89 90
    }

91 92 93 94 95
    fun resetDirty() {
        val values = ContentValues(1)
        values.put(ContactsContract.RawContacts.DIRTY, 0)
        addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
    }
96

97 98 99 100
    override fun updateFlags(flags: Int) {
        val values = ContentValues(1)
        values.put(LocalContact.COLUMN_FLAGS, flags)
        addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
101

102
        this.flags = flags
103 104 105 106 107 108 109 110 111 112
    }


    override fun populateData(mimeType: String, row: ContentValues) {
        when (mimeType) {
            CachedGroupMembership.CONTENT_ITEM_TYPE ->
                cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
            GroupMembership.CONTENT_ITEM_TYPE ->
                groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID))
            UnknownProperties.CONTENT_ITEM_TYPE ->
113
                contact!!.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
114 115 116 117 118 119
        }
    }

    override fun insertDataRows(batch: BatchOperation) {
        super.insertDataRows(batch)

120 121 122 123 124 125 126 127
        contact!!.unknownProperties?.let { unknownProperties ->
            val op: BatchOperation.Operation
            val builder = ContentProviderOperation.newInsert(dataSyncURI())
            if (id == null)
                op = BatchOperation.Operation(builder, UnknownProperties.RAW_CONTACT_ID, 0)
            else {
                op = BatchOperation.Operation(builder)
                builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
128
            }
129 130 131
            builder .withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
                    .withValue(UnknownProperties.UNKNOWN_PROPERTIES, unknownProperties)
            batch.enqueue(op)
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
        }
    }


    /**
     * Calculates a hash code from the contact's data (VCard) and group memberships.
     * Attention: re-reads {@link #contact} from the database, discarding all changes in memory
     * @return hash code of contact data (including group memberships)
     */
    internal fun dataHashCode(): Int {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            throw IllegalStateException("dataHashCode() should not be called on Android != 7")

        // reset contact so that getContact() reads from database
        contact = null

        // groupMemberships is filled by getContact()
        val dataHash = contact!!.hashCode()
        val groupHash = groupMemberships.hashCode()
Ricki Hirner's avatar
Ricki Hirner committed
151
        Logger.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
152 153 154 155 156 157 158 159
        return dataHash xor groupHash
    }

    fun updateHashCode(batch: BatchOperation?) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            throw IllegalStateException("updateHashCode() should not be called on Android != 7")

        val values = ContentValues(1)
160 161 162
        val hashCode = dataHashCode()
        Logger.log.fine("Storing contact hash = $hashCode")
        values.put(COLUMN_HASHCODE, hashCode)
163

164 165 166 167 168 169 170
        if (batch == null)
            addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
        else {
            val builder = ContentProviderOperation
                    .newUpdate(rawContactSyncURI())
                    .withValues(values)
            batch.enqueue(BatchOperation.Operation(builder))
171 172 173 174 175 176 177
        }
    }

    fun getLastHashCode(): Int {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            throw IllegalStateException("getLastHashCode() should not be called on Android != 7")

178 179 180
        addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
            if (c.moveToNext() && !c.isNull(0))
                return c.getInt(0)
181
        }
182
        return 0
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
    }


    fun addToGroup(batch: BatchOperation, groupID: Long) {
        batch.enqueue(BatchOperation.Operation(
                ContentProviderOperation.newInsert(dataSyncURI())
                        .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
                        .withValue(GroupMembership.RAW_CONTACT_ID, id)
                        .withValue(GroupMembership.GROUP_ROW_ID, groupID)
        ))
        groupMemberships.add(groupID)

        batch.enqueue(BatchOperation.Operation(
                ContentProviderOperation.newInsert(dataSyncURI())
                        .withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
                        .withValue(CachedGroupMembership.RAW_CONTACT_ID, id)
                        .withValue(CachedGroupMembership.GROUP_ID, groupID)
                        .withYieldAllowed(true)
        ))
        cachedGroupMemberships.add(groupID)
    }

    fun removeGroupMemberships(batch: BatchOperation) {
        batch.enqueue(BatchOperation.Operation(
                ContentProviderOperation.newDelete(dataSyncURI())
                        .withSelection(
                                Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + " IN (?,?)",
                                arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
                        )
                        .withYieldAllowed(true)
        ))
        groupMemberships.clear()
        cachedGroupMemberships.clear()
    }

    /**
     * Returns the IDs of all groups the contact was member of (cached memberships).
     * Cached memberships are kept in sync with memberships by DAVdroid and are used to determine
     * whether a membership has been deleted/added when a raw contact is dirty.
     * @return set of {@link GroupMembership#GROUP_ROW_ID} (may be empty)
223 224
     * @throws FileNotFoundException if the current contact can't be found
     * @throws RemoteException on contacts provider errors
225 226 227 228 229 230 231 232 233
     */
    fun getCachedGroupMemberships(): Set<Long> {
        contact
        return cachedGroupMemberships
    }

    /**
     * Returns the IDs of all groups the contact is member of.
     * @return set of {@link GroupMembership#GROUP_ROW_ID}s (may be empty)
234 235
     * @throws FileNotFoundException if the current contact can't be found
     * @throws RemoteException on contacts provider errors
236 237 238 239 240 241 242 243 244 245
     */
    fun getGroupMemberships(): Set<Long> {
        contact
        return groupMemberships
    }


    // factory

    object Factory: AndroidContactFactory<LocalContact> {
246 247
        override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
                LocalContact(addressBook, values)
248 249 250
    }

}