package de.measite.contactmerger.contacts;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import de.measite.contactmerger.contacts.StatusUpdate.Presence;
import de.measite.contactmerger.util.ShiftedExpireLRU;

/**
 * <p>The ContactsDataMapper is responsible for mapping {@link RawContact} and
 * {@link Metadata} and Metadata subclasses.</p>
 */
public class ContactDataMapper {

    /*
     * Having a data mapper has advantages and disadvantages.
     *
     * Advantages:
     * - Central provider interaction
     * - Clear OOP preference for the model
     *
     * Disadvantages:
     * - Structural overhead
     * - Performance penalty
     *
     * Looks like a no-datamapper attitude would be better. Except when you
     * start to look into the contacts api, at which point you'll start to love
     * OOP on top of the API.
     *
     * You have been warned, braindead code ahead.
     */

    /**
     * The contacts content provider client.
     */
    private final ContentProviderClient provider;

    /**
     * An optional cache.
     */
    private Map<MethodCall, Object> cache = null;

    public void setCache(ShiftedExpireLRU cache) {
        this.cache = cache;
    }

    public static class MethodCall {
        public final long created;
        public final String method;
        public final Object[] args;
        public MethodCall(String method, Object ... args) {
            this.method = method;
            this.args = args;
            this.created = System.currentTimeMillis();
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            MethodCall that = (MethodCall) o;

            // Probably incorrect - comparing Object[] arrays with Arrays.equals
            if (!Arrays.equals(args, that.args)) return false;
            if (!method.equals(that.method)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = method.hashCode();
            result = 31 * result + Arrays.hashCode(args);
            return result;
        }
    }

    /**
     * Create a new data mapper on top of a given contacts provider client.
     * @param provider A ContentProviderClient for the Contacts ContentProvider.
     */
    public ContactDataMapper(ContentProviderClient provider) {
        this.provider = provider;
    }

    public ContactDataMapper(Context context) {
        this.provider =
                context.getContentResolver().acquireContentProviderClient(
                        ContactsContract.AUTHORITY_URI);
    }
    /**
     * Perform a set of operations.
     * @param operations A list of pending operations to be executed.
     */
    public void perform(ArrayList<ContentProviderOperation> operations) {
        try {
            provider.applyBatch(operations);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (OperationApplicationException e) {
            e.printStackTrace();
        }
    }

    /**
     * Save a single Contact with the attached Metadata into the phonebook.
     * @param contact The contact to e saved.
     */
    public void persist(RawContact contact) {
        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
        persist(contact, operations);
        perform(operations);
    }

    /**
     * Append all operations needed to store the current contact to a set of
     * operations.
     * @param contact The current contact with metadata.
     * @param operations A set of operations to be extended.
     */
    public void persist(RawContact contact, ArrayList<ContentProviderOperation> operations) {
        int operationsStart = operations.size();
        Builder operation;
        if (contact.getID() == -1) {
            operation = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
        } else {
            operation = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI);
            operation.withSelection(RawContacts._ID + "=?", new String[]{Long.toString(contact.getID())});
        }
        ContentValues values = new ContentValues();
        put(values, contact);
        operation.withValues(values);
        operations.add(operation.build());
        for (Metadata data: contact.getMetadata().values()) {
            values.clear();
            put(values, data);
            if (data instanceof DeletedMetadata) {
                operation = ContentProviderOperation.newDelete(Data.CONTENT_URI);
                operation.withValues(values);
                operation.withSelection(Data._ID + "=?", new String[]{Long.toString(contact.getID())});
                operations.add(operation.build());
                continue;
            }
            if (data.getID() == -1) {
                operation = ContentProviderOperation.newInsert(Data.CONTENT_URI);
            } else {
                operation = ContentProviderOperation.newUpdate(Data.CONTENT_URI);
                operation.withSelection(Data._ID + "=?", new String[]{Long.toString(data.getID())});
            }
            if (contact.getID() == -1) {
                operation.withValueBackReference(Data.RAW_CONTACT_ID, operationsStart);
                values.remove(Data.RAW_CONTACT_ID);
            } else {
                values.put(Data.RAW_CONTACT_ID, contact.getID());
            }
            operation.withValues(values);
            operations.add(operation.build());
        }
    }

    /**
     * Save a single status update.
     * @param statusUpdate The status update to be stored.
     */
    public void persist(StatusUpdate statusUpdate) {
        ContentValues values = new ContentValues();
        try {
            put(values, statusUpdate);
            provider.insert(StatusUpdates.CONTENT_URI, values);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * Fetch a users status based on a account and user jid.
     * @param accountJid The account jid (aka your local jid).
     * @param jid The jid in question (aka the remote jid).
     * @return A new StatusUpdate instance, or null.
     */
    public StatusUpdate getStatusUpdate(String accountJid, String jid) {
        // $%&# ok, so status updates will do magic
        // Solution? use the Data magic, and fill the missing blocks.
        // IM_HANDLER and IM_ACCOUNT is virtual anyway....
        try {
            Cursor cursor = provider.query(
                    Data.CONTENT_URI,
                    STATUS_UPDATES_IMPLICIT_PROJECTION_MAP,
                    Data.MIMETYPE + "=? AND " +
                    Data.SYNC2 + "=? AND " +
                    Data.SYNC3 + "=?",
                    new String[]{ImMetadata.MIMETYPE, accountJid, jid},
                    null);
            if (!cursor.moveToFirst()) {
                cursor.close();
                StatusUpdate update = new StatusUpdate();
                update.setImAccount(accountJid);
                update.setImHandle(jid);
                return update;
            }

            StatusUpdate statusUpdate = newStatusUpdate(cursor);
            cursor.close();

            statusUpdate.setImAccount(accountJid);
            statusUpdate.setImHandle(jid);

            return statusUpdate;

        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * <p>Delete a set of contacts based on their id.</p>
     * <p><i>Note:</i> the method used for bulk delete is a group selection
     * based on id (<i>{@ling BaseColumns#_ID} IN (id1, id2, ...)</i>).
     * @param ids The IDs if all users that should be deleted.
     */
    public void bulkDelete(long[] ids) {
        if (ids.length == 0) {
            return;
        }
        StringBuilder where = new StringBuilder();
        where.append(RawContacts._ID);
        where.append(" IN (");
        where.append(Long.toString(ids[0]));
        for (int i = 1; i < ids.length; i++) {
            where.append(',');
            where.append(Long.toString(ids[i]));
        }
        where.append(')');
        try {
            provider.delete(RawContacts.CONTENT_URI, where.toString(), null);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    public int[] getContactIDs() {
        int[] result = null;
        try {
            Cursor cursor = provider.query(
                    Contacts.CONTENT_URI,
                    CONTACT_ID_PROJECTION_MAP,
                    null,
                    null,
                    null);
            int count = cursor.getCount();
            result = new int[count];
            int pos = 0;
            try {
                int index = 0;
                if (cursor.moveToFirst()) {
                    index = cursor.getColumnIndex(Contacts._ID);
                    result[pos++] = cursor.getInt(index);
                }
                cursor.moveToNext();
                while (!cursor.isAfterLast()) {
                    result[pos++] = cursor.getInt(index);
                    cursor.moveToNext();
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
            return null;
        }
        return result;
    }

    private final static String[] CONTACT_OF_RAW_CONTACT_PROJECTION =
            new String[]{RawContacts.CONTACT_ID};

    public int getContactByRawContactID(long id) {
        int result = -1;
        try {
            Cursor cursor = provider.query(
                    RawContacts.CONTENT_URI,
                    CONTACT_OF_RAW_CONTACT_PROJECTION,
                    RawContacts._ID + "=" + id,
                    null,
                    null);
            try {
                if (cursor.moveToFirst()) {
                    result = cursor.getInt(cursor.getColumnIndex(
                                RawContacts.CONTACT_ID));
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return result;
    }

    public Contact getContactById(long id, boolean rawContacts, boolean metadata) {
        Contact contact = null;

        if (cache != null) {
            MethodCall call = new MethodCall("getContactById", id, rawContacts, metadata);
            contact = (Contact)cache.get(call);
            if (contact != null) return contact;
        }

        // step 1, load contact
        try {
            Cursor cursor = provider.query(
                Contacts.CONTENT_URI,
                CONTACT_PROJECTION_MAP, 
                Contacts._ID + "=" + id,
                null,
                null);
            try {
                if (cursor.moveToFirst()) {
                    contact = newContact(cursor);
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        if (contact == null || !rawContacts) {
            return contact;
        }
        try {
            Cursor cursor = provider.query(
                    RawContacts.CONTENT_URI,
                    RAW_CONTACT_PROJECTION_MAP, 
                    RawContacts.CONTACT_ID + "=" + id,
                    null,
                    null);
            try {
                if (cursor.moveToFirst()) {
                    RawContact raw[] = new RawContact[cursor.getCount()];
                    int pos = 0;
                    RawContact rc = newRawContact(cursor);
                    if (metadata) {
                        fetchMetadata(rc);
                    }
                    raw[pos++] = rc;
                    cursor.moveToNext();
                    while (!cursor.isAfterLast()) {
                        rc = newRawContact(cursor);
                        if (metadata) {
                            fetchMetadata(rc);
                        }
                        raw[pos++] = rc;
                        cursor.moveToNext();
                    }
                    contact.setRawContacts(raw);
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        if (contact != null && cache != null) {
            MethodCall call = new MethodCall("getContactById", id, rawContacts, metadata);
            cache.put(call, contact);
        }

        return contact;
    }

    /**
     * Retrieve a single jid as bound by a local account jid, with or without
     * metadata.
     * @param accountJid The local account jid.
     * @param jid The remote jid.
     * @param metadata True if a second fetch for metadata should be done.
     * @return A single contact.
     */
    public RawContact getRawContactByJid(String accountJid, String jid, boolean metadata) {
        RawContact contact = null;
        try {
            Cursor cursor = provider.query(
                    RawContacts.CONTENT_URI,
                    RAW_CONTACT_PROJECTION_MAP, 
                    RawContacts.ACCOUNT_NAME + "=? AND " +
                    RawContacts.SOURCE_ID + "=?",
                    new String[]{accountJid, jid},
                    null);
            try {
                if (cursor.moveToFirst()) {
                    contact = newRawContact(cursor);
                    if (metadata) {
                        fetchMetadata(contact);
                    }
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return contact;
    }

    /**
     * Fetch the metadata of a single account. All results will be attached
     * to the contact.
     * @param contact The contact that should be enriched.
     */
    private void fetchMetadata(RawContact contact) {
        try {
            Cursor cursor = provider.query(
                    Data.CONTENT_URI,
                    DATA_PROJECTION_MAP, 
                    Data.RAW_CONTACT_ID + "=?",
                    new String[]{Long.toString(contact.getID())},
                    null);
            try {
                if (cursor.moveToFirst()) {
                    do {
                        contact.setMetadata(newMetadata(cursor));
                    } while (cursor.moveToNext());
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * The projection map used to query the Data table for status update
     * information.
     */
    private static String[] STATUS_UPDATES_IMPLICIT_PROJECTION_MAP = new String[]{
        Data._ID,
        // These variables trigger a left join
        StatusUpdates.PRESENCE,
        StatusUpdates.STATUS,
        StatusUpdates.STATUS_TIMESTAMP,
        StatusUpdates.STATUS_RES_PACKAGE,
        StatusUpdates.STATUS_LABEL,
        StatusUpdates.STATUS_ICON,
    };

    private static String[] CONTACT_PROJECTION_MAP = new String[]{
        ContactsContract.Contacts._ID,
        ContactsContract.Contacts.DISPLAY_NAME,
        ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE,
        ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
        ContactsContract.Contacts.DISPLAY_NAME_SOURCE,
        ContactsContract.Contacts.PHONETIC_NAME,
        ContactsContract.Contacts.PHONETIC_NAME_STYLE,
        ContactsContract.Contacts.PHOTO_FILE_ID,
        ContactsContract.Contacts.PHOTO_ID,
        ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
        ContactsContract.Contacts.PHOTO_URI,
        ContactsContract.Contacts.SORT_KEY_ALTERNATIVE,
        ContactsContract.Contacts.SORT_KEY_PRIMARY,
    };

    private static String[] CONTACT_ID_PROJECTION_MAP = new String[]{
        ContactsContract.Contacts._ID
    };

    /**
     * Projection map used for contact fetch.
     */
    private static String[] RAW_CONTACT_PROJECTION_MAP = new String[]{
        RawContacts._ID,
        RawContacts.ACCOUNT_NAME,
        RawContacts.ACCOUNT_TYPE,
        RawContacts.SOURCE_ID,
        RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4
    };

    /**
     * Projection map used for metadata fetches.
     */
    private static String[] DATA_PROJECTION_MAP = new String[]{
        Data._ID,
        Data.RAW_CONTACT_ID,
        Data.MIMETYPE,
        Data.SYNC1,  Data.SYNC2,  Data.SYNC3,  Data.SYNC4,
        Data.DATA1,  Data.DATA2,  Data.DATA3,  Data.DATA4,  Data.DATA5,
        Data.DATA6,  Data.DATA7,  Data.DATA8,  Data.DATA9,  Data.DATA10,
        Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15
    };

    /**
     * Projection map for raw contact sync fields.
     */
    private static String[] RAW_CONTACTS_SYNC_FIELDS = new String[]{
        RawContacts.SYNC1, RawContacts.SYNC2,  RawContacts.SYNC3,  RawContacts.SYNC4
    };

    /**
     * Proijection map for data sync fields.
     */
    private static String[] SYNC_FIELDS = new String[]{
        Data.SYNC1,  Data.SYNC2,  Data.SYNC3,  Data.SYNC4
    };

    /**
     * Projection map for metadata data fields.
     */
    private static String[] DATA_FIELDS = new String[]{
        Data.DATA1,  Data.DATA2,  Data.DATA3,  Data.DATA4,  Data.DATA5,
        Data.DATA6,  Data.DATA7,  Data.DATA8,  Data.DATA9,  Data.DATA10,
        Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14
    };

    /**
     * Store all metadata info into a given contentvalues instance.
     * @param values A ContentValues instance.
     * @param metadata The Metadata instance to be saved to ContentValues.
     */
    private void put(ContentValues values, Metadata metadata) {
        if (metadata.getID() > 0) {
            values.put(Data._ID, metadata.getID());
        }
        if (metadata.getRawContactID() > 0) {
            values.put(Data.RAW_CONTACT_ID, metadata.getRawContactID());
        }
        values.put(Data.MIMETYPE, metadata.getMimetype());
        for (int i = 0; i < SYNC_FIELDS.length; i++) {
            values.put(SYNC_FIELDS[i], metadata.getSync(i));
        }
        for (int i = 0; i < DATA_FIELDS.length; i++) {
            values.put(DATA_FIELDS[i], metadata.getData(i));
        }
        values.put(Data.DATA15, metadata.getBlob());
    }

    /**
     * Add all contact data to a content values instance.
     * @param values The ContentValues instance.
     * @param contact The contact to be copied into the values parameter.
     */
    private void put(ContentValues values, RawContact contact) {
        if (contact.getID() > 0) {
            values.put(RawContacts._ID, contact.getID());
        }
        values.put(RawContacts.ACCOUNT_NAME, contact.getAccountName());
        values.put(RawContacts.ACCOUNT_TYPE, contact.getAccountType());
        values.put(RawContacts.SOURCE_ID, contact.getSourceID());
        for (int i = 0; i < RAW_CONTACTS_SYNC_FIELDS.length; i++) {
            values.put(RAW_CONTACTS_SYNC_FIELDS[i], contact.getSync(i));
        }
    }

    /**
     * Add all status update values to a given ContentValues instance.
     * @param values The ContentValues instance.
     * @param statusUpdate The status update to be copied.
     */
    private void put(ContentValues values, StatusUpdate statusUpdate) {
        if (statusUpdate.getDataId() >= 0l) {
            values.put(StatusUpdates.DATA_ID, statusUpdate.getDataId());
        }
        values.put(StatusUpdates.IM_ACCOUNT, statusUpdate.getImAccount());
        values.put(StatusUpdates.IM_HANDLE, statusUpdate.getImHandle());
        values.put(StatusUpdates.STATUS, statusUpdate.getStatus());
        values.put(StatusUpdates.PRESENCE, statusUpdate.getPresence().getValue());
        values.put(StatusUpdates.PROTOCOL, statusUpdate.getProtocol().getValue());
    }

    /**
     * Create a new status update based on the current cursor.
     * @param cursor The current DB cursor.
     * @return A new StatusUpdate instance.
     */
    private StatusUpdate newStatusUpdate(Cursor cursor) {
        StatusUpdate statusUpdate = new StatusUpdate();

        int index = cursor.getColumnIndex(Data._ID);
        statusUpdate.setDataId(cursor.getLong(index));

        index = cursor.getColumnIndex(StatusUpdates.PRESENCE);
        statusUpdate.setPresence(Presence.byPresenceId(cursor.getInt(index)));

        index = cursor.getColumnIndex(StatusUpdates.STATUS);
        statusUpdate.setStatus(cursor.getString(index));

        return statusUpdate;
    }

    private Contact newContact(Cursor cursor) {
        Contact contact = new Contact();

        int index = cursor.getColumnIndex(Contacts._ID);
        contact.setId(cursor.getLong(index));

        index = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
        contact.setDisplayName(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.DISPLAY_NAME_ALTERNATIVE);
        contact.setDisplayNameAlternative(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.DISPLAY_NAME_PRIMARY);
        contact.setDisplayNamePrimary(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.DISPLAY_NAME_SOURCE);
        contact.setDisplayNameSource(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.PHONETIC_NAME_STYLE);
        contact.setPhoneticNameStyle(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.PHONETIC_NAME);
        contact.setPhoneticName(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.PHOTO_FILE_ID);
        contact.setPhotoFileId(cursor.getLong(index));

        index = cursor.getColumnIndex(Contacts.PHOTO_ID);
        contact.setPhotoId(cursor.getLong(index));

        index = cursor.getColumnIndex(Contacts.PHOTO_THUMBNAIL_URI);
        contact.setPhotoThumbnailUri(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.PHOTO_URI);
        contact.setPhotoUri(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.SORT_KEY_ALTERNATIVE);
        contact.setSortKeyAlternative(cursor.getString(index));

        index = cursor.getColumnIndex(Contacts.SORT_KEY_PRIMARY);
        contact.setSortKeyPrimary(cursor.getString(index));

        return contact;
    }

    /**
     * Create a new raw contact for the current cursor.
     * @param cursor The DB cursor, scrolled to the row in question.
     * @return
     */
    private RawContact newRawContact(Cursor cursor) {
        RawContact contact = new RawContact();
        int index = cursor.getColumnIndex(RawContacts._ID);
        contact.setID(cursor.getLong(index));

        index = cursor.getColumnIndex(RawContacts.ACCOUNT_NAME);
        contact.setAccountName(cursor.getString(index));

        index = cursor.getColumnIndex(RawContacts.ACCOUNT_TYPE);
        contact.setAccountType(cursor.getString(index));

        index = cursor.getColumnIndex(RawContacts.SOURCE_ID);
        contact.setSourceID(cursor.getString(index));

        for (int i = 0; i < RAW_CONTACTS_SYNC_FIELDS.length; i++) {
            index = cursor.getColumnIndex(RAW_CONTACTS_SYNC_FIELDS[i]);
            contact.setSync(i, cursor.getString(index));
        }
        return contact;
    }

    /**
     * Create a new Metadata instance based on the current db curser.
     * @param cursor The current Db cursor, scrolled to the metadata in
     *               question.
     * @return A new Metadata instance.
     */
    private Metadata newMetadata(Cursor cursor) {
        /*
         * This method has high anti-pattern potential. Why?
         * It's cross-referencing the whole Metadata inheritance tree.
         *
         * TODO: Move to a factory.
         */
        Metadata metadata = null;
        int index = cursor.getColumnIndex(Data.MIMETYPE);
        String mimetype = cursor.getString(index);
        if (NicknameMetadata.MIMETYPE.equals(mimetype)) {
            metadata = new NicknameMetadata();
        }
        if (metadata == null && ImMetadata.MIMETYPE.equals(mimetype)) {
            metadata = new ImMetadata();
        }
        if (metadata == null && PhotoMetadata.MIMETYPE.equals(mimetype)) {
            metadata = new PhotoMetadata();
        }
        if (metadata == null && StructuredNameMetadata.MIMETYPE.equals(mimetype)) {
            metadata = new StructuredNameMetadata();
        }
        if (metadata == null) {
            metadata = new Metadata();
            metadata.setMimetype(cursor.getString(index));
        }

        index = cursor.getColumnIndex(Data._ID);
        metadata.setID(cursor.getLong(index));

        index = cursor.getColumnIndex(Data.RAW_CONTACT_ID);
        metadata.setRawContactID(cursor.getLong(index));

        for (int i = 0; i < SYNC_FIELDS.length; i++) {
            index = cursor.getColumnIndex(SYNC_FIELDS[i]);
            metadata.setSync(i, cursor.getString(index));
        }
        for (int i = 0; i < DATA_FIELDS.length; i++) {
            index = cursor.getColumnIndex(DATA_FIELDS[i]);
            metadata.setData(i, cursor.getString(index));
        }
        index = cursor.getColumnIndex(Data.DATA15);
        metadata.setBlob(cursor.getBlob(index));

        return metadata;
    }

    public void setAggregationMode(long id1, long id2, int value) {
        try {
            ContentValues values = new ContentValues();
            values.put(AggregationExceptions.TYPE, value);
            values.put(AggregationExceptions.RAW_CONTACT_ID1, id1);
            values.put(AggregationExceptions.RAW_CONTACT_ID2, id2);
            provider.update(
                    AggregationExceptions.CONTENT_URI,
                    values,
                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + id1 + " AND " +
                    AggregationExceptions.RAW_CONTACT_ID2 + "=" + id2,
                    null
            );
            values.put(AggregationExceptions.RAW_CONTACT_ID1, id2);
            values.put(AggregationExceptions.RAW_CONTACT_ID2, id1);
            provider.update(
                    AggregationExceptions.CONTENT_URI,
                    values,
                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + id2 + " AND " +
                    AggregationExceptions.RAW_CONTACT_ID2 + "=" + id1,
                    null
            );
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    public int getAggregationMode(long id1, long id2) {
        int result = AggregationExceptions.TYPE_AUTOMATIC;
        try {
            Cursor cursor = provider.query(
                    AggregationExceptions.CONTENT_URI,
                    new String[]{AggregationExceptions.TYPE}, 
                    AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " +
                    AggregationExceptions.RAW_CONTACT_ID2 + "=?",
                    new String[]{Long.toString(id1), Long.toString(id2)},
                    null);
            try {
                if (cursor.moveToFirst()) {
                    result = cursor.getInt(cursor.getColumnIndex(AggregationExceptions.TYPE));
                }
            } finally {
                cursor.close();
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return result;
    }

}