org.kontalk.sync.Syncer.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.sync.Syncer.java

Source

/*
 * Kontalk Android client
 * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
    
 * This program 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.
    
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.kontalk.sync;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils;
import org.jxmpp.util.XmppStringUtils;
import org.spongycastle.openpgp.PGPPublicKey;

import android.accounts.Account;
import android.accounts.OperationCanceledException;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;

import org.kontalk.BuildConfig;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.PGP;
import org.kontalk.crypto.PGPUserID;
import org.kontalk.data.Contact;
import org.kontalk.provider.Keyring;
import org.kontalk.provider.MyUsers;
import org.kontalk.provider.MyUsers.Users;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.util.XMPPUtils;

/**
 * The syncer core.
 * @author Daniele Ricci
 */
public class Syncer {
    // using SyncAdapter tag
    private static final String TAG = SyncAdapter.TAG;

    // max time to wait for network response
    private static final int MAX_WAIT_TIME = 60000;

    /** {@link Data} column for the display name. */
    public static final String DATA_COLUMN_DISPLAY_NAME = Data.DATA1;
    /** {@link Data} column for the account name. */
    public static final String DATA_COLUMN_ACCOUNT_NAME = Data.DATA2;
    /** {@link Data} column for the phone number. */
    public static final String DATA_COLUMN_PHONE = Data.DATA3;

    /** {@link RawContacts} column for the display name. */
    public static final String RAW_COLUMN_DISPLAY_NAME = RawContacts.SYNC1;
    /** {@link RawContacts} column for the phone number. */
    public static final String RAW_COLUMN_PHONE = RawContacts.SYNC2;
    /** {@link RawContacts} column for the JID. */
    public static final String RAW_COLUMN_USERID = RawContacts.SYNC3;

    /** Random packet id used for requesting public keys. */
    static final String IQ_PACKET_ID = StringUtils.randomString(10);

    private volatile boolean mCanceled;
    private final Context mContext;

    private final static class PresenceItem {
        public String from;
        public String status;
        public String rosterName;
        public long timestamp;
        public byte[] publicKey;
        public boolean blocked;
        public boolean presence;
        /** True if found during roster match. */
        public boolean matched;
        /** Discard this entry: it has not been found on server. */
        public boolean discarded;
    }

    // FIXME this class should handle most recent/available presence stanzas
    private static final class PresenceBroadcastReceiver extends BroadcastReceiver {
        /** Max number of items in a roster match request. */
        private static final int MAX_ROSTER_MATCH_SIZE = 500;

        private List<PresenceItem> response;
        private final WeakReference<Syncer> notifyTo;

        private final List<String> jidList;
        private int rosterParts = -1;
        private String[] iq;
        private String presenceId;

        private int presenceCount;
        private int pubkeyCount;
        private int rosterCount;
        /** Packet id list for not matched contacts (in roster but not matched on server). */
        private Set<String> notMatched = new HashSet<>();
        private boolean blocklistReceived;

        public PresenceBroadcastReceiver(List<String> jidList, Syncer notifyTo) {
            this.notifyTo = new WeakReference<>(notifyTo);
            this.jidList = jidList;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (MessageCenterService.ACTION_PRESENCE.equals(action)) {

                // consider only presences received *after* roster response
                if (response != null && presenceId != null) {

                    String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
                    String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
                    String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
                    if (type != null && presenceId.equals(id)) {
                        // update presence item data
                        String bareJid = XmppStringUtils.parseBareJid(jid);
                        PresenceItem item = getPresenceItem(bareJid);
                        item.status = intent.getStringExtra(MessageCenterService.EXTRA_STATUS);
                        item.timestamp = intent.getLongExtra(MessageCenterService.EXTRA_STAMP, -1);
                        item.rosterName = intent.getStringExtra(MessageCenterService.EXTRA_ROSTER_NAME);
                        if (!item.presence) {
                            item.presence = true;
                            // increment presence count
                            presenceCount++;
                            // check user existance (only if subscription is "both")
                            if (!item.matched
                                    && intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_FROM, false)
                                    && intent.getBooleanExtra(MessageCenterService.EXTRA_SUBSCRIBED_TO, false)) {
                                // verify actual user existance through last activity
                                String lastActivityId = StringUtils.randomString(6);
                                MessageCenterService.requestLastActivity(context, item.from, lastActivityId);
                                notMatched.add(lastActivityId);
                            }
                        }
                    }
                }
            }

            // roster match result received
            else if (MessageCenterService.ACTION_ROSTER_MATCH.equals(action)) {
                String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
                for (String iqId : iq) {
                    if (iqId.equals(id)) {
                        // decrease roster parts counter
                        rosterParts--;

                        String[] list = intent.getStringArrayExtra(MessageCenterService.EXTRA_JIDLIST);
                        if (list != null) {
                            rosterCount += list.length;
                            if (response == null) {
                                // prepare list to be filled in with presence data
                                response = new ArrayList<>(rosterCount);
                            }
                            for (String jid : list) {
                                PresenceItem p = new PresenceItem();
                                p.from = jid;
                                p.matched = true;
                                response.add(p);
                            }
                        }

                        if (rosterParts <= 0) {
                            // all roster parts received

                            if (rosterCount == 0 && blocklistReceived) {
                                // no roster elements
                                finish();
                            } else {
                                Syncer w = notifyTo.get();
                                if (w != null) {
                                    // request presence data for the whole roster
                                    presenceId = StringUtils.randomString(6);
                                    w.requestPresenceData(presenceId);
                                    // request public keys for the whole roster
                                    w.requestPublicKeys();
                                    // request block list
                                    w.requestBlocklist();
                                }
                            }
                        }

                        // no need to continue
                        break;
                    }
                }
            }

            else if (MessageCenterService.ACTION_PUBLICKEY.equals(action)) {
                if (response != null) {
                    String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
                    // see if bare JID is present in roster response
                    String compare = XmppStringUtils.parseBareJid(jid);
                    for (PresenceItem item : response) {
                        if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(compare)) {
                            item.publicKey = intent.getByteArrayExtra(MessageCenterService.EXTRA_PUBLIC_KEY);

                            // increment vcard count
                            pubkeyCount++;
                            break;
                        }
                    }

                    // done with presence data and blocklist
                    if (pubkeyCount == presenceCount && blocklistReceived && notMatched.size() == 0)
                        finish();

                }

            }

            else if (MessageCenterService.ACTION_BLOCKLIST.equals(action)) {
                blocklistReceived = true;

                String[] list = intent.getStringArrayExtra(MessageCenterService.EXTRA_BLOCKLIST);
                if (list != null) {

                    for (String jid : list) {
                        // see if bare JID is present in roster response
                        String compare = XmppStringUtils.parseBareJid(jid);
                        for (PresenceItem item : response) {
                            if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(compare)) {
                                item.blocked = true;

                                break;
                            }
                        }
                    }

                }

                // done with presence data and blocklist
                if (pubkeyCount >= presenceCount && notMatched.size() == 0)
                    finish();
            }

            // last activity (for user existance verification)
            else if (MessageCenterService.ACTION_LAST_ACTIVITY.equals(action)) {
                String requestId = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID);
                if (notMatched.contains(requestId)) {
                    notMatched.remove(requestId);

                    String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
                    // consider only item-not-found (404) errors
                    if (type != null && type.equalsIgnoreCase(IQ.Type.error.toString())
                            && XMPPError.Condition.item_not_found.toString()
                                    .equals(intent.getStringExtra(MessageCenterService.EXTRA_ERROR_CONDITION))) {
                        // user does not exist!
                        String jid = intent.getStringExtra(MessageCenterService.EXTRA_FROM);
                        // discard entry
                        discardPresenceItem(jid);
                        // unsubscribe!
                        unsubscribe(context, jid);

                        if (pubkeyCount >= presenceCount && blocklistReceived && notMatched.size() == 0)
                            finish();
                    }
                }
            }

            // connected! Retry...
            else if (MessageCenterService.ACTION_CONNECTED.equals(action) && rosterParts < 0) {
                Syncer w = notifyTo.get();
                if (w != null) {
                    // request a roster match
                    rosterParts = getRosterParts(jidList);
                    iq = new String[rosterParts];
                    for (int i = 0; i < rosterParts; i++) {
                        int end = (i + 1) * MAX_ROSTER_MATCH_SIZE;
                        if (end >= jidList.size())
                            end = jidList.size();
                        List<String> slice = jidList.subList(i * MAX_ROSTER_MATCH_SIZE, end);

                        iq[i] = StringUtils.randomString(6);
                        w.requestRosterMatch(iq[i], slice);
                    }
                }
            }
        }

        private void discardPresenceItem(String jid) {
            for (PresenceItem item : response) {
                if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(jid)) {
                    item.discarded = true;
                    return;
                }
            }
        }

        private PresenceItem getPresenceItem(String jid) {
            for (PresenceItem item : response) {
                if (XmppStringUtils.parseBareJid(item.from).equalsIgnoreCase(jid))
                    return item;
            }

            // add item if not found
            PresenceItem item = new PresenceItem();
            item.from = jid;
            response.add(item);
            return item;
        }

        private void unsubscribe(Context context, String jid) {
            Intent i = new Intent(context, MessageCenterService.class);
            i.setAction(MessageCenterService.ACTION_PRESENCE);
            i.putExtra(MessageCenterService.EXTRA_TO, jid);
            i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.unsubscribe.name());
            context.startService(i);
        }

        private int getRosterParts(List<String> jidList) {
            return (int) Math.ceil((double) jidList.size() / MAX_ROSTER_MATCH_SIZE);
        }

        public List<PresenceItem> getResponse() {
            return (rosterCount >= 0) ? response : null;
        }

        private void finish() {
            Syncer w = notifyTo.get();
            if (w != null) {
                synchronized (w) {
                    w.notifyAll();
                }
            }
        }
    }

    public Syncer(Context context) {
        mContext = context;
    }

    public void onSyncCanceled() {
        mCanceled = true;
    }

    public void onSyncResumed() {
        mCanceled = false;
    }

    private static final class RawPhoneNumberEntry {
        public final String number;
        public final String jid;
        public final String lookupKey;

        public RawPhoneNumberEntry(String lookupKey, String number, String jid) {
            this.lookupKey = lookupKey;
            this.number = number;
            this.jid = jid;
        }
    }

    /**
     * The actual sync procedure.
     * This one uses the slowest method ever: it first checks for every phone
     * number in all contacts and it sends them to the server. Once a response
     * is received, it deletes all the raw contacts created by us and then
     * recreates only the ones the server has found a match for.
     */
    public void performSync(Context context, Account account, String authority, ContentProviderClient provider,
            ContentProviderClient usersProvider, SyncResult syncResult) throws OperationCanceledException {

        final Map<String, RawPhoneNumberEntry> lookupNumbers = new HashMap<>();
        final List<String> jidList = new ArrayList<>();

        // resync users database
        Log.v(TAG, "resyncing users database");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // update users database
        Uri uri = Users.CONTENT_URI.buildUpon().appendQueryParameter(Users.RESYNC, "true").build();
        try {
            int count = usersProvider.update(uri, new ContentValues(), null, null);
            Log.d(TAG, "users database resynced (" + count + ")");
        } catch (Exception e) {
            Log.e(TAG, "error resyncing users database - aborting sync", e);
            syncResult.databaseError = true;
            return;
        }

        // query all contacts
        Cursor cursor;
        try {
            cursor = usersProvider.query(Users.CONTENT_URI_OFFLINE,
                    new String[] { Users.JID, Users.NUMBER, Users.LOOKUP_KEY }, null, null, null);
        } catch (Exception e) {
            Log.e(TAG, "error querying users database - aborting sync", e);
            syncResult.databaseError = true;
            return;
        }

        while (cursor.moveToNext()) {
            if (mCanceled) {
                cursor.close();
                throw new OperationCanceledException();
            }

            String jid = cursor.getString(0);
            String number = cursor.getString(1);
            String lookupKey = cursor.getString(2);

            // avoid to send duplicates to the server
            if (lookupNumbers.put(XmppStringUtils.parseLocalpart(jid),
                    new RawPhoneNumberEntry(lookupKey, number, jid)) == null)
                jidList.add(jid);
        }
        cursor.close();

        if (mCanceled)
            throw new OperationCanceledException();

        // empty contacts :-|
        if (jidList.size() == 0) {
            // delete all Kontalk raw contacts
            try {
                syncResult.stats.numDeletes += deleteAll(account, provider);
            } catch (Exception e) {
                Log.e(TAG, "contact delete error", e);
                syncResult.databaseError = true;
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                try {
                    syncResult.stats.numDeletes += deleteProfile(account, provider);
                } catch (Exception e) {
                    Log.e(TAG, "profile delete error", e);
                    syncResult.databaseError = true;
                }
            }

            commit(usersProvider, syncResult);
        }

        else {
            final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mContext);

            // register presence broadcast receiver
            PresenceBroadcastReceiver receiver = new PresenceBroadcastReceiver(jidList, this);
            IntentFilter f = new IntentFilter();
            f.addAction(MessageCenterService.ACTION_PRESENCE);
            f.addAction(MessageCenterService.ACTION_ROSTER_MATCH);
            f.addAction(MessageCenterService.ACTION_PUBLICKEY);
            f.addAction(MessageCenterService.ACTION_BLOCKLIST);
            f.addAction(MessageCenterService.ACTION_LAST_ACTIVITY);
            f.addAction(MessageCenterService.ACTION_CONNECTED);
            lbm.registerReceiver(receiver, f);

            // request current connection status
            MessageCenterService.requestConnectionStatus(mContext);

            // wait for the service to complete its job
            synchronized (this) {
                // wait for connection
                try {
                    wait(MAX_WAIT_TIME);
                } catch (InterruptedException e) {
                    // simulate canceled operation
                    mCanceled = true;
                }
            }

            lbm.unregisterReceiver(receiver);

            // last chance to quit
            if (mCanceled)
                throw new OperationCanceledException();

            List<PresenceItem> res = receiver.getResponse();
            if (res != null) {
                ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
                // TODO operations.size() could be used instead (?)
                int op = 0;

                // this is the time - delete all Kontalk raw contacts
                try {
                    syncResult.stats.numDeletes += deleteAll(account, provider);
                } catch (Exception e) {
                    Log.e(TAG, "contact delete error", e);
                    syncResult.databaseError = true;
                    return;
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                    try {
                        syncResult.stats.numDeletes += deleteProfile(account, provider);
                    } catch (Exception e) {
                        Log.e(TAG, "profile delete error", e);
                        syncResult.databaseError = true;
                    }
                }

                ContentValues registeredValues = new ContentValues();
                registeredValues.put(Users.REGISTERED, 1);
                for (int i = 0; i < res.size(); i++) {
                    PresenceItem entry = res.get(i);
                    if (entry.discarded)
                        continue;

                    final RawPhoneNumberEntry data = lookupNumbers.get(XmppStringUtils.parseLocalpart(entry.from));
                    if (data != null && data.lookupKey != null) {
                        // add contact
                        addContact(account, getDisplayName(provider, data.lookupKey, data.number), data.number,
                                data.jid, operations, op++);
                    } else {
                        syncResult.stats.numSkippedEntries++;
                    }

                    // update fields
                    try {
                        String status = entry.status;

                        if (!TextUtils.isEmpty(status))
                            registeredValues.put(Users.STATUS, status);
                        else
                            registeredValues.putNull(Users.STATUS);

                        if (entry.timestamp >= 0)
                            registeredValues.put(Users.LAST_SEEN, entry.timestamp);
                        else
                            registeredValues.putNull(Users.LAST_SEEN);

                        if (entry.publicKey != null) {
                            try {
                                PGPPublicKey pubKey = PGP.getMasterKey(entry.publicKey);
                                // trust our own key blindly
                                int trustLevel = Authenticator.isSelfJID(mContext, entry.from)
                                        ? MyUsers.Keys.TRUST_VERIFIED
                                        : -1;
                                // update keys table immediately
                                Keyring.setKey(mContext, entry.from, entry.publicKey, trustLevel);

                                // no data from system contacts, use name from public key
                                if (data == null) {
                                    PGPUserID uid = PGP.parseUserId(pubKey,
                                            XmppStringUtils.parseDomain(entry.from));
                                    if (uid != null) {
                                        registeredValues.put(Users.DISPLAY_NAME, uid.getName());
                                    }
                                }
                            } catch (Exception e) {
                                Log.w(TAG, "unable to parse public key", e);
                            }
                        } else {
                            // use roster name if no contact data available
                            if (data == null && entry.rosterName != null) {
                                registeredValues.put(Users.DISPLAY_NAME, entry.rosterName);
                            }
                        }

                        // blocked status
                        registeredValues.put(Users.BLOCKED, entry.blocked);
                        // user JID as reported by the server
                        registeredValues.put(Users.JID, entry.from);

                        /*
                         * Since UsersProvider.resync inserted the user row
                         * using our server name, it might have changed because
                         * of what the server reported. We already put into the
                         * values the new JID, but we need to use the old one
                         * in the where condition so we will have a match.
                         */
                        String origJid;
                        if (data != null)
                            origJid = XMPPUtils.createLocalJID(mContext,
                                    XmppStringUtils.parseLocalpart(entry.from));
                        else
                            origJid = entry.from;
                        usersProvider.update(Users.CONTENT_URI_OFFLINE, registeredValues, Users.JID + " = ?",
                                new String[] { origJid });

                        // clear data
                        registeredValues.remove(Users.DISPLAY_NAME);

                        // if this is our own contact, trust our own key later
                        if (Authenticator.isSelfJID(mContext, entry.from)) {
                            // register our profile while we're at it
                            if (data != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                                // add contact
                                addProfile(account, Authenticator.getDefaultDisplayName(mContext), data.number,
                                        data.jid, operations, op++);
                            }
                        }
                    } catch (Exception e) {
                        Log.e(TAG, "error updating users database", e);
                        // we shall continue here...
                    }
                }

                try {
                    if (operations.size() > 0)
                        provider.applyBatch(operations);
                    syncResult.stats.numInserts += op;
                    syncResult.stats.numEntries += op;
                } catch (Exception e) {
                    Log.w(TAG, "contact write error", e);
                    syncResult.stats.numSkippedEntries += op;
                    /*
                     * We do not consider system contacts failure a fatal error.
                     * This is actually a workaround for systems with disabled permissions or
                     * exotic firmwares. It can also protect against security 3rd party apps or
                     * non-Android platforms, such as Jolla/Alien Dalvik.
                     */
                }

                commit(usersProvider, syncResult);
            }

            // timeout or error
            else {
                Log.w(TAG, "connection timeout - aborting sync");

                syncResult.stats.numIoExceptions++;
            }
        }
    }

    private void commit(ContentProviderClient usersProvider, SyncResult syncResult) {
        // commit users table
        Uri uri = Users.CONTENT_URI.buildUpon().appendQueryParameter(Users.RESYNC, "true")
                .appendQueryParameter(Users.COMMIT, "true").build();
        try {
            usersProvider.update(uri, null, null, null);
            Log.d(TAG, "users database committed");
            Contact.invalidate();
        } catch (Exception e) {
            Log.e(TAG, "error committing users database - aborting sync", e);
            syncResult.databaseError = true;
        }
    }

    private void requestRosterMatch(String id, List<String> list) {
        Intent i = new Intent(mContext, MessageCenterService.class);
        i.setAction(MessageCenterService.ACTION_ROSTER_MATCH);
        i.putExtra(MessageCenterService.EXTRA_PACKET_ID, id);
        i.putExtra(MessageCenterService.EXTRA_JIDLIST, list.toArray(new String[list.size()]));
        mContext.startService(i);
    }

    private void requestPresenceData(String id) {
        Intent i = new Intent(mContext, MessageCenterService.class);
        i.setAction(MessageCenterService.ACTION_PRESENCE);
        i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.probe.toString());
        i.putExtra(MessageCenterService.EXTRA_PACKET_ID, id);
        mContext.startService(i);
    }

    private void requestPublicKeys() {
        Intent i = new Intent(mContext, MessageCenterService.class);
        i.setAction(MessageCenterService.ACTION_PUBLICKEY);
        i.putExtra(MessageCenterService.EXTRA_PACKET_ID, IQ_PACKET_ID);
        mContext.startService(i);
    }

    private void requestBlocklist() {
        Intent i = new Intent(mContext, MessageCenterService.class);
        i.setAction(MessageCenterService.ACTION_BLOCKLIST);
        mContext.startService(i);
    }

    private String getDisplayName(ContentProviderClient client, String lookupKey, String defaultValue) {
        String displayName = null;
        Cursor nameQuery = null;
        try {
            nameQuery = client.query(Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey),
                    new String[] { ContactsContract.Contacts.DISPLAY_NAME }, null, null, null);
            if (nameQuery.moveToFirst())
                displayName = nameQuery.getString(0);
        } catch (Exception e) {
            // ignored
        } finally {
            // close cursor
            try {
                nameQuery.close();
            } catch (Exception ignored) {
            }
        }

        return (displayName != null) ? displayName : defaultValue;
    }

    private int deleteAll(Account account, ContentProviderClient provider) throws RemoteException {
        return provider.delete(RawContacts.CONTENT_URI.buildUpon()
                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build(), null, null);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private int deleteProfile(Account account, ContentProviderClient provider) throws RemoteException {
        return provider.delete(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI.buildUpon()
                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
                .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type).build(), null, null);
    }

    /*
    private int deleteContact(Account account, long rawContactId) {
    Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)
        .buildUpon()
        .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
        .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
        .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
        .build();
    ContentProviderClient client = mContext.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    try {
        return client.delete(uri, null, null);
    }
    catch (RemoteException e) {
        Log.e(TAG, "delete error", e);
    }
    finally {
        client.release();
    }
        
    return -1;
    }
    */

    private void addContact(Account account, String username, String phone, String jid,
            List<ContentProviderOperation> operations, int index) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "adding contact \"" + username + "\" <" + phone + ">");
        }

        // create our RawContact
        operations.add(insertRawContact(account, username, phone, jid, RawContacts.CONTENT_URI).build());

        // add contact data
        addContactData(username, phone, operations, index);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void addProfile(Account account, String username, String phone, String jid,
            List<ContentProviderOperation> operations, int index) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "adding profile \"" + username + "\" <" + phone + ">");
        }

        // create our RawContact
        operations.add(
                insertRawContact(account, username, phone, jid, ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI)
                        .build());

        // add contact data
        addContactData(username, phone, operations, index);
    }

    private ContentProviderOperation.Builder insertRawContact(Account account, String username, String phone,
            String jid, Uri uri) {
        return ContentProviderOperation.newInsert(uri)
                .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT)
                .withValue(RawContacts.ACCOUNT_NAME, account.name).withValue(RawContacts.ACCOUNT_TYPE, account.type)
                .withValue(RAW_COLUMN_DISPLAY_NAME, username).withValue(RAW_COLUMN_PHONE, phone)
                .withValue(RAW_COLUMN_USERID, jid);
    }

    private void addContactData(String username, String phone, List<ContentProviderOperation> operations,
            int index) {
        ContentProviderOperation.Builder builder;
        final int opIndex = index * 3;

        // create a Data record of common type 'StructuredName' for our RawContact
        builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, opIndex)
                .withValue(ContactsContract.Data.MIMETYPE,
                        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
                .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, username);
        operations.add(builder.build());

        // create a Data record of custom type 'org.kontalk.user' to display a link to the conversation
        builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, opIndex)
                .withValue(ContactsContract.Data.MIMETYPE, Users.CONTENT_ITEM_TYPE)
                .withValue(DATA_COLUMN_DISPLAY_NAME, username)
                .withValue(DATA_COLUMN_ACCOUNT_NAME, mContext.getString(R.string.app_name))
                .withValue(DATA_COLUMN_PHONE, phone).withYieldAllowed(true);
        operations.add(builder.build());
    }

}