com.android.contacts.ContactSaveService.java Source code

Java tutorial

Introduction

Here is the source code for com.android.contacts.ContactSaveService.java

Source

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts;

import static android.Manifest.permission.WRITE_CONTACTS;

import android.app.Activity;
import android.app.IntentService;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.android.contacts.activities.ContactEditorActivity;
import com.android.contacts.compat.CompatUtils;
import com.android.contacts.compat.PinnedPositionsCompat;
import com.android.contacts.database.ContactUpdateUtils;
import com.android.contacts.database.SimContactDao;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.CPOWrapper;
import com.android.contacts.model.RawContactDelta;
import com.android.contacts.model.RawContactDeltaList;
import com.android.contacts.model.RawContactModifier;
import com.android.contacts.model.account.AccountWithDataSet;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.util.ContactDisplayUtils;
import com.android.contacts.util.ContactPhotoUtils;
import com.android.contacts.util.PermissionsUtil;
import com.android.contactsbind.FeedbackHelper;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * A service responsible for saving changes to the content provider.
 */
public class ContactSaveService extends IntentService {
    private static final String TAG = "ContactSaveService";

    /** Set to true in order to view logs on content provider operations */
    private static final boolean DEBUG = false;

    public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";

    public static final String EXTRA_ACCOUNT_NAME = "accountName";
    public static final String EXTRA_ACCOUNT_TYPE = "accountType";
    public static final String EXTRA_DATA_SET = "dataSet";
    public static final String EXTRA_ACCOUNT = "account";
    public static final String EXTRA_CONTENT_VALUES = "contentValues";
    public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
    public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
    public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";

    public static final String ACTION_SAVE_CONTACT = "saveContact";
    public static final String EXTRA_CONTACT_STATE = "state";
    public static final String EXTRA_SAVE_MODE = "saveMode";
    public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
    public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
    public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";

    public static final String ACTION_CREATE_GROUP = "createGroup";
    public static final String ACTION_RENAME_GROUP = "renameGroup";
    public static final String ACTION_DELETE_GROUP = "deleteGroup";
    public static final String ACTION_UPDATE_GROUP = "updateGroup";
    public static final String EXTRA_GROUP_ID = "groupId";
    public static final String EXTRA_GROUP_LABEL = "groupLabel";
    public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
    public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";

    public static final String ACTION_SET_STARRED = "setStarred";
    public static final String ACTION_DELETE_CONTACT = "delete";
    public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
    public static final String EXTRA_CONTACT_URI = "contactUri";
    public static final String EXTRA_CONTACT_IDS = "contactIds";
    public static final String EXTRA_STARRED_FLAG = "starred";
    public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
    public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";

    public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
    public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
    public static final String EXTRA_DATA_ID = "dataId";

    public static final String ACTION_SPLIT_CONTACT = "splitContact";
    public static final String EXTRA_HARD_SPLIT = "extraHardSplit";

    public static final String ACTION_JOIN_CONTACTS = "joinContacts";
    public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
    public static final String EXTRA_CONTACT_ID1 = "contactId1";
    public static final String EXTRA_CONTACT_ID2 = "contactId2";

    public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
    public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";

    public static final String ACTION_SET_RINGTONE = "setRingtone";
    public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";

    public static final String ACTION_UNDO = "undo";
    public static final String EXTRA_UNDO_ACTION = "undoAction";
    public static final String EXTRA_UNDO_DATA = "undoData";

    // For debugging and testing what happens when requests are queued up.
    public static final String ACTION_SLEEP = "sleep";
    public static final String EXTRA_SLEEP_DURATION = "sleepDuration";

    public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
    public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
    public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";

    public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";

    public static final String EXTRA_RESULT_CODE = "resultCode";
    public static final String EXTRA_RESULT_COUNT = "count";

    public static final int CP2_ERROR = 0;
    public static final int CONTACTS_LINKED = 1;
    public static final int CONTACTS_SPLIT = 2;
    public static final int BAD_ARGUMENTS = 3;
    public static final int RESULT_UNKNOWN = 0;
    public static final int RESULT_SUCCESS = 1;
    public static final int RESULT_FAILURE = 2;

    private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(Data.MIMETYPE, Data.IS_PRIMARY,
            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);

    private static final int PERSIST_TRIES = 3;

    private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;

    public interface Listener {
        public void onServiceCompleted(Intent callbackIntent);
    }

    private static final CopyOnWriteArrayList<Listener> sListeners = new CopyOnWriteArrayList<Listener>();

    // Holds the current state of the service
    private static final State sState = new State();

    private Handler mMainHandler;
    private GroupsDao mGroupsDao;
    private SimContactDao mSimContactDao;

    public ContactSaveService() {
        super(TAG);
        setIntentRedelivery(true);
        mMainHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mGroupsDao = new GroupsDaoImpl(this);
        mSimContactDao = SimContactDao.create(this);
    }

    public static void registerListener(Listener listener) {
        if (!(listener instanceof Activity)) {
            throw new ClassCastException("Only activities can be registered to" + " receive callback from "
                    + ContactSaveService.class.getName());
        }
        sListeners.add(0, listener);
    }

    public static boolean canUndo(Intent resultIntent) {
        return resultIntent.hasExtra(EXTRA_UNDO_DATA);
    }

    public static void unregisterListener(Listener listener) {
        sListeners.remove(listener);
    }

    public static State getState() {
        return sState;
    }

    private void notifyStateChanged() {
        LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
    }

    /**
     * Returns true if the ContactSaveService was started successfully and false if an exception
     * was thrown and a Toast error message was displayed.
     */
    public static boolean startService(Context context, Intent intent, int saveMode) {
        try {
            context.startService(intent);
        } catch (Exception exception) {
            final int resId;
            switch (saveMode) {
            case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
                resId = R.string.contactUnlinkErrorToast;
                break;
            case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
                resId = R.string.contactJoinErrorToast;
                break;
            case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
                resId = R.string.contactSavedErrorToast;
                break;
            default:
                resId = R.string.contactGenericErrorToast;
            }
            Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    /**
     * Utility method that starts service and handles exception.
     */
    public static void startService(Context context, Intent intent) {
        try {
            context.startService(intent);
        } catch (Exception exception) {
            Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public Object getSystemService(String name) {
        Object service = super.getSystemService(name);
        if (service != null) {
            return service;
        }

        return getApplicationContext().getSystemService(name);
    }

    // Parent classes Javadoc says not to override this method but we're doing it just to update
    // our state which should be OK since we're still doing the work in onHandleIntent
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        sState.onStart(intent);
        notifyStateChanged();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    protected void onHandleIntent(final Intent intent) {
        if (intent == null) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "onHandleIntent: could not handle null intent");
            }
            return;
        }
        if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
            Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
            // TODO: add more specific error string such as "Turn on Contacts
            // permission to update your contacts"
            showToast(R.string.contactSavedErrorToast);
            return;
        }

        // Call an appropriate method. If we're sure it affects how incoming phone calls are
        // handled, then notify the fact to in-call screen.
        String action = intent.getAction();
        if (ACTION_NEW_RAW_CONTACT.equals(action)) {
            createRawContact(intent);
        } else if (ACTION_SAVE_CONTACT.equals(action)) {
            saveContact(intent);
        } else if (ACTION_CREATE_GROUP.equals(action)) {
            createGroup(intent);
        } else if (ACTION_RENAME_GROUP.equals(action)) {
            renameGroup(intent);
        } else if (ACTION_DELETE_GROUP.equals(action)) {
            deleteGroup(intent);
        } else if (ACTION_UPDATE_GROUP.equals(action)) {
            updateGroup(intent);
        } else if (ACTION_SET_STARRED.equals(action)) {
            setStarred(intent);
        } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
            setSuperPrimary(intent);
        } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
            clearPrimary(intent);
        } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
            deleteMultipleContacts(intent);
        } else if (ACTION_DELETE_CONTACT.equals(action)) {
            deleteContact(intent);
        } else if (ACTION_SPLIT_CONTACT.equals(action)) {
            splitContact(intent);
        } else if (ACTION_JOIN_CONTACTS.equals(action)) {
            joinContacts(intent);
        } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
            joinSeveralContacts(intent);
        } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
            setSendToVoicemail(intent);
        } else if (ACTION_SET_RINGTONE.equals(action)) {
            setRingtone(intent);
        } else if (ACTION_UNDO.equals(action)) {
            undo(intent);
        } else if (ACTION_SLEEP.equals(action)) {
            sleepForDebugging(intent);
        }

        sState.onFinish(intent);
        notifyStateChanged();
    }

    /**
     * Creates an intent that can be sent to this service to create a new raw contact
     * using data presented as a set of ContentValues.
     */
    public static Intent createNewRawContactIntent(Context context, ArrayList<ContentValues> values,
            AccountWithDataSet account, Class<? extends Activity> callbackActivity, String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
        if (account != null) {
            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
            serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
            serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
        }
        serviceIntent.putParcelableArrayListExtra(ContactSaveService.EXTRA_CONTENT_VALUES, values);

        // Callback intent will be invoked by the service once the new contact is
        // created.  The service will put the URI of the new contact as "data" on
        // the callback intent.
        Intent callbackIntent = new Intent(context, callbackActivity);
        callbackIntent.setAction(callbackAction);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
        return serviceIntent;
    }

    private void createRawContact(Intent intent) {
        String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
        String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
        String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
        List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);

        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
        operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
                .withValue(RawContacts.ACCOUNT_NAME, accountName).withValue(RawContacts.ACCOUNT_TYPE, accountType)
                .withValue(RawContacts.DATA_SET, dataSet).build());

        int size = valueList.size();
        for (int i = 0; i < size; i++) {
            ContentValues values = valueList.get(i);
            values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
            operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
                    .withValueBackReference(Data.RAW_CONTACT_ID, 0).withValues(values).build());
        }

        ContentResolver resolver = getContentResolver();
        ContentProviderResult[] results;
        try {
            results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
        } catch (Exception e) {
            throw new RuntimeException("Failed to store new contact", e);
        }

        Uri rawContactUri = results[0].uri;
        callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));

        deliverCallback(callbackIntent);
    }

    /**
     * Creates an intent that can be sent to this service to create a new raw contact
     * using data presented as a set of ContentValues.
     * This variant is more convenient to use when there is only one photo that can
     * possibly be updated, as in the Contact Details screen.
     * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
     * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
     */
    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
            String saveModeExtraKey, int saveMode, boolean isProfile, Class<? extends Activity> callbackActivity,
            String callbackAction, long rawContactId, Uri updatedPhotoPath) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
        return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile, callbackActivity,
                callbackAction, bundle, /* joinContactIdExtraKey */ null, /* joinContactId */ null);
    }

    /**
     * Creates an intent that can be sent to this service to create a new raw contact
     * using data presented as a set of ContentValues.
     * This variant is used when multiple contacts' photos may be updated, as in the
     * Contact Editor.
     *
     * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
     * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
     * @param joinContactId the raw contact ID to join to the contact after doing the save.
     */
    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
            String saveModeExtraKey, int saveMode, boolean isProfile, Class<? extends Activity> callbackActivity,
            String callbackAction, Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
        serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
        serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
        serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);

        if (updatedPhotos != null) {
            serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
        }

        if (callbackActivity != null) {
            // Callback intent will be invoked by the service once the contact is
            // saved.  The service will put the URI of the new contact as "data" on
            // the callback intent.
            Intent callbackIntent = new Intent(context, callbackActivity);
            callbackIntent.putExtra(saveModeExtraKey, saveMode);
            if (joinContactIdExtraKey != null && joinContactId != null) {
                callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
            }
            callbackIntent.setAction(callbackAction);
            serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
        }
        return serviceIntent;
    }

    private void saveContact(Intent intent) {
        RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
        boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
        Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);

        if (state == null) {
            Log.e(TAG, "Invalid arguments for saveContact request");
            return;
        }

        int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
        // Trim any empty fields, and RawContacts, before persisting
        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
        RawContactModifier.trimEmpty(state, accountTypes);

        Uri lookupUri = null;

        final ContentResolver resolver = getContentResolver();

        boolean succeeded = false;

        // Keep track of the id of a newly raw-contact (if any... there can be at most one).
        long insertedRawContactId = -1;

        // Attempt to persist changes
        int tries = 0;
        while (tries++ < PERSIST_TRIES) {
            try {
                // Build operations and try applying
                final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();

                final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();

                for (CPOWrapper cpoWrapper : diffWrapper) {
                    diff.add(cpoWrapper.getOperation());
                }

                if (DEBUG) {
                    Log.v(TAG, "Content Provider Operations:");
                    for (ContentProviderOperation operation : diff) {
                        Log.v(TAG, operation.toString());
                    }
                }

                int numberProcessed = 0;
                boolean batchFailed = false;
                final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
                while (numberProcessed < diff.size()) {
                    final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
                    if (subsetCount == -1) {
                        Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
                        batchFailed = true;
                        break;
                    } else {
                        numberProcessed += subsetCount;
                    }
                }

                if (batchFailed) {
                    // Retry save
                    continue;
                }

                final long rawContactId = getRawContactId(state, diffWrapper, results);
                if (rawContactId == -1) {
                    throw new IllegalStateException("Could not determine RawContact ID after save");
                }
                // We don't have to check to see if the value is still -1.  If we reach here,
                // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
                insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
                if (isProfile) {
                    // Since the profile supports local raw contacts, which may have been completely
                    // removed if all information was removed, we need to do a special query to
                    // get the lookup URI for the profile contact (if it still exists).
                    Cursor c = resolver.query(Profile.CONTENT_URI,
                            new String[] { Contacts._ID, Contacts.LOOKUP_KEY }, null, null, null);
                    if (c == null) {
                        continue;
                    }
                    try {
                        if (c.moveToFirst()) {
                            final long contactId = c.getLong(0);
                            final String lookupKey = c.getString(1);
                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
                        }
                    } finally {
                        c.close();
                    }
                } else {
                    final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
                    lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
                }
                if (lookupUri != null && Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Saved contact. New URI: " + lookupUri);
                }

                // We can change this back to false later, if we fail to save the contact photo.
                succeeded = true;
                break;

            } catch (RemoteException e) {
                // Something went wrong, bail without success
                FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
                break;

            } catch (IllegalArgumentException e) {
                // This is thrown by applyBatch on malformed requests
                FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
                showToast(R.string.contactSavedErrorToast);
                break;

            } catch (OperationApplicationException e) {
                // Version consistency failed, re-parent change and try again
                Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
                final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
                boolean first = true;
                final int count = state.size();
                for (int i = 0; i < count; i++) {
                    Long rawContactId = state.getRawContactId(i);
                    if (rawContactId != null && rawContactId != -1) {
                        if (!first) {
                            sb.append(',');
                        }
                        sb.append(rawContactId);
                        first = false;
                    }
                }
                sb.append(")");

                if (first) {
                    throw new IllegalStateException("Version consistency failed for a new contact", e);
                }

                final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
                        isProfile ? RawContactsEntity.PROFILE_CONTENT_URI : RawContactsEntity.CONTENT_URI, resolver,
                        sb.toString(), null, null);
                state = RawContactDeltaList.mergeAfter(newState, state);

                // Update the new state to use profile URIs if appropriate.
                if (isProfile) {
                    for (RawContactDelta delta : state) {
                        delta.setProfileQueryUri();
                    }
                }
            }
        }

        // Now save any updated photos.  We do this at the end to ensure that
        // the ContactProvider already knows about newly-created contacts.
        if (updatedPhotos != null) {
            for (String key : updatedPhotos.keySet()) {
                Uri photoUri = updatedPhotos.getParcelable(key);
                long rawContactId = Long.parseLong(key);

                // If the raw-contact ID is negative, we are saving a new raw-contact;
                // replace the bogus ID with the new one that we actually saved the contact at.
                if (rawContactId < 0) {
                    rawContactId = insertedRawContactId;
                }

                // If the save failed, insertedRawContactId will be -1
                if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
                    succeeded = false;
                }
            }
        }

        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        if (callbackIntent != null) {
            if (succeeded) {
                // Mark the intent to indicate that the save was successful (even if the lookup URI
                // is now null).  For local contacts or the local profile, it's possible that the
                // save triggered removal of the contact, so no lookup URI would exist..
                callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
            }
            callbackIntent.setData(lookupUri);
            deliverCallback(callbackIntent);
        }
    }

    /**
     * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
     * subsets, adds the returned array to "results".
     *
     * @return the size of the array, if not null; -1 when the array is null.
     */
    private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
            ContentProviderResult[] results, ContentResolver resolver)
            throws RemoteException, OperationApplicationException {
        final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
        final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
        subset.addAll(diff.subList(offset, offset + subsetCount));
        final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract.AUTHORITY, subset);
        if (subsetResult == null || (offset + subsetResult.length) > results.length) {
            return -1;
        }
        for (ContentProviderResult c : subsetResult) {
            results[offset++] = c;
        }
        return subsetResult.length;
    }

    /**
     * Save updated photo for the specified raw-contact.
     * @return true for success, false for failure
     */
    private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
        final Uri outputUri = Uri.withAppendedPath(
                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
                RawContacts.DisplayPhoto.CONTENT_DIRECTORY);

        return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
    }

    /**
     * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
     */
    private long getRawContactId(RawContactDeltaList state, final ArrayList<CPOWrapper> diffWrapper,
            final ContentProviderResult[] results) {
        long existingRawContactId = state.findRawContactId();
        if (existingRawContactId != -1) {
            return existingRawContactId;
        }

        return getInsertedRawContactId(diffWrapper, results);
    }

    /**
     * Find the ID of a newly-inserted raw-contact.  If none exists, return -1.
     */
    private long getInsertedRawContactId(final ArrayList<CPOWrapper> diffWrapper,
            final ContentProviderResult[] results) {
        if (results == null) {
            return -1;
        }
        final int diffSize = diffWrapper.size();
        final int numResults = results.length;
        for (int i = 0; i < diffSize && i < numResults; i++) {
            final CPOWrapper cpoWrapper = diffWrapper.get(i);
            final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
            if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath()
                    .contains(RawContacts.CONTENT_URI.getEncodedPath())) {
                return ContentUris.parseId(results[i].uri);
            }
        }
        return -1;
    }

    /**
     * Creates an intent that can be sent to this service to create a new group as
     * well as add new members at the same time.
     *
     * @param context of the application
     * @param account in which the group should be created
     * @param label is the name of the group (cannot be null)
     * @param rawContactsToAdd is an array of raw contact IDs for contacts that
     *            should be added to the group
     * @param callbackActivity is the activity to send the callback intent to
     * @param callbackAction is the intent action for the callback intent
     */
    public static Intent createNewGroupIntent(Context context, AccountWithDataSet account, String label,
            long[] rawContactsToAdd, Class<? extends Activity> callbackActivity, String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
        serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
        serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);

        // Callback intent will be invoked by the service once the new group is
        // created.
        Intent callbackIntent = new Intent(context, callbackActivity);
        callbackIntent.setAction(callbackAction);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);

        return serviceIntent;
    }

    private void createGroup(Intent intent) {
        String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
        String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
        String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
        final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);

        // Create the new group
        final Uri groupUri = mGroupsDao.create(label, new AccountWithDataSet(accountName, accountType, dataSet));
        final ContentResolver resolver = getContentResolver();

        // If there's no URI, then the insertion failed. Abort early because group members can't be
        // added if the group doesn't exist
        if (groupUri == null) {
            Log.e(TAG, "Couldn't create group with label " + label);
            return;
        }

        // Add new group members
        addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));

        ContentValues values = new ContentValues();
        // TODO: Move this into the contact editor where it belongs. This needs to be integrated
        // with the way other intent extras that are passed to the
        // {@link ContactEditorActivity}.
        values.clear();
        values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
        values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));

        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        callbackIntent.setData(groupUri);
        // TODO: This can be taken out when the above TODO is addressed
        callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
        deliverCallback(callbackIntent);
    }

    /**
     * Creates an intent that can be sent to this service to rename a group.
     */
    public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
            Class<? extends Activity> callbackActivity, String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);

        // Callback intent will be invoked by the service once the group is renamed.
        Intent callbackIntent = new Intent(context, callbackActivity);
        callbackIntent.setAction(callbackAction);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);

        return serviceIntent;
    }

    private void renameGroup(Intent intent) {
        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);

        if (groupId == -1) {
            Log.e(TAG, "Invalid arguments for renameGroup request");
            return;
        }

        ContentValues values = new ContentValues();
        values.put(Groups.TITLE, label);
        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
        getContentResolver().update(groupUri, values, null, null);

        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        callbackIntent.setData(groupUri);
        deliverCallback(callbackIntent);
    }

    /**
     * Creates an intent that can be sent to this service to delete a group.
     */
    public static Intent createGroupDeletionIntent(Context context, long groupId) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);

        return serviceIntent;
    }

    private void deleteGroup(Intent intent) {
        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
        if (groupId == -1) {
            Log.e(TAG, "Invalid arguments for deleteGroup request");
            return;
        }
        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);

        final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
        final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
        callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
        callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);

        mGroupsDao.delete(groupUri);

        LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
    }

    public static Intent createUndoIntent(Context context, Intent resultIntent) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
        serviceIntent.putExtras(resultIntent);
        return serviceIntent;
    }

    private void undo(Intent intent) {
        final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
        if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
            mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
        }
    }

    /**
     * Creates an intent that can be sent to this service to rename a group as
     * well as add and remove members from the group.
     *
     * @param context of the application
     * @param groupId of the group that should be modified
     * @param newLabel is the updated name of the group (can be null if the name
     *            should not be updated)
     * @param rawContactsToAdd is an array of raw contact IDs for contacts that
     *            should be added to the group
     * @param rawContactsToRemove is an array of raw contact IDs for contacts
     *            that should be removed from the group
     * @param callbackActivity is the activity to send the callback intent to
     * @param callbackAction is the intent action for the callback intent
     */
    public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
            long[] rawContactsToAdd, long[] rawContactsToRemove, Class<? extends Activity> callbackActivity,
            String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE, rawContactsToRemove);

        // Callback intent will be invoked by the service once the group is updated
        Intent callbackIntent = new Intent(context, callbackActivity);
        callbackIntent.setAction(callbackAction);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);

        return serviceIntent;
    }

    private void updateGroup(Intent intent) {
        long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
        long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
        long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);

        if (groupId == -1) {
            Log.e(TAG, "Invalid arguments for updateGroup request");
            return;
        }

        final ContentResolver resolver = getContentResolver();
        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);

        // Update group name if necessary
        if (label != null) {
            ContentValues values = new ContentValues();
            values.put(Groups.TITLE, label);
            resolver.update(groupUri, values, null, null);
        }

        // Add and remove members if necessary
        addMembersToGroup(resolver, rawContactsToAdd, groupId);
        removeMembersFromGroup(resolver, rawContactsToRemove, groupId);

        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        callbackIntent.setData(groupUri);
        deliverCallback(callbackIntent);
    }

    private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd, long groupId) {
        if (rawContactsToAdd == null) {
            return;
        }
        for (long rawContactId : rawContactsToAdd) {
            try {
                final ArrayList<ContentProviderOperation> rawContactOperations = new ArrayList<ContentProviderOperation>();

                // Build an assert operation to ensure the contact is not already in the group
                final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
                        .newAssertQuery(Data.CONTENT_URI);
                assertBuilder.withSelection(
                        Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID
                                + "=?",
                        new String[] { String.valueOf(rawContactId), GroupMembership.CONTENT_ITEM_TYPE,
                                String.valueOf(groupId) });
                assertBuilder.withExpectedCount(0);
                rawContactOperations.add(assertBuilder.build());

                // Build an insert operation to add the contact to the group
                final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
                        .newInsert(Data.CONTENT_URI);
                insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
                insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
                insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
                rawContactOperations.add(insertBuilder.build());

                if (DEBUG) {
                    for (ContentProviderOperation operation : rawContactOperations) {
                        Log.v(TAG, operation.toString());
                    }
                }

                // Apply batch
                if (!rawContactOperations.isEmpty()) {
                    resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
                }
            } catch (RemoteException e) {
                // Something went wrong, bail without success
                FeedbackHelper.sendFeedback(this, TAG,
                        "Problem persisting user edits for raw contact ID " + String.valueOf(rawContactId), e);
            } catch (OperationApplicationException e) {
                // The assert could have failed because the contact is already in the group,
                // just continue to the next contact
                FeedbackHelper.sendFeedback(this, TAG, "Assert failed in adding raw contact ID "
                        + String.valueOf(rawContactId) + ". Already exists in group " + String.valueOf(groupId), e);
            }
        }
    }

    private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove, long groupId) {
        if (rawContactsToRemove == null) {
            return;
        }
        for (long rawContactId : rawContactsToRemove) {
            // Apply the delete operation on the data row for the given raw contact's
            // membership in the given group. If no contact matches the provided selection, then
            // nothing will be done. Just continue to the next contact.
            resolver.delete(Data.CONTENT_URI,
                    Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID
                            + "=?",
                    new String[] { String.valueOf(rawContactId), GroupMembership.CONTENT_ITEM_TYPE,
                            String.valueOf(groupId) });
        }
    }

    /**
     * Creates an intent that can be sent to this service to star or un-star a contact.
     */
    public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
        serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);

        return serviceIntent;
    }

    private void setStarred(Intent intent) {
        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
        boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
        if (contactUri == null) {
            Log.e(TAG, "Invalid arguments for setStarred request");
            return;
        }

        final ContentValues values = new ContentValues(1);
        values.put(Contacts.STARRED, value);
        getContentResolver().update(contactUri, values, null, null);

        // Undemote the contact if necessary
        final Cursor c = getContentResolver().query(contactUri, new String[] { Contacts._ID }, null, null, null);
        if (c == null) {
            return;
        }
        try {
            if (c.moveToFirst()) {
                final long id = c.getLong(0);

                // Don't bother undemoting if this contact is the user's profile.
                if (id < Profile.MIN_ID) {
                    PinnedPositionsCompat.undemote(getContentResolver(), id);
                }
            }
        } finally {
            c.close();
        }
    }

    /**
     * Creates an intent that can be sent to this service to set the redirect to voicemail.
     */
    public static Intent createSetSendToVoicemail(Context context, Uri contactUri, boolean value) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
        serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);

        return serviceIntent;
    }

    private void setSendToVoicemail(Intent intent) {
        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
        boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
        if (contactUri == null) {
            Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
            return;
        }

        final ContentValues values = new ContentValues(1);
        values.put(Contacts.SEND_TO_VOICEMAIL, value);
        getContentResolver().update(contactUri, values, null, null);
    }

    /**
     * Creates an intent that can be sent to this service to save the contact's ringtone.
     */
    public static Intent createSetRingtone(Context context, Uri contactUri, String value) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);

        return serviceIntent;
    }

    private void setRingtone(Intent intent) {
        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
        String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
        if (contactUri == null) {
            Log.e(TAG, "Invalid arguments for setRingtone");
            return;
        }
        ContentValues values = new ContentValues(1);
        values.put(Contacts.CUSTOM_RINGTONE, value);
        getContentResolver().update(contactUri, values, null, null);
    }

    /**
     * Creates an intent that sets the selected data item as super primary (default)
     */
    public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
        return serviceIntent;
    }

    private void setSuperPrimary(Intent intent) {
        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
        if (dataId == -1) {
            Log.e(TAG, "Invalid arguments for setSuperPrimary request");
            return;
        }

        ContactUpdateUtils.setSuperPrimary(this, dataId);
    }

    /**
     * Creates an intent that clears the primary flag of all data items that belong to the same
     * raw_contact as the given data item. Will only clear, if the data item was primary before
     * this call
     */
    public static Intent createClearPrimaryIntent(Context context, long dataId) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
        serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
        return serviceIntent;
    }

    private void clearPrimary(Intent intent) {
        long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
        if (dataId == -1) {
            Log.e(TAG, "Invalid arguments for clearPrimary request");
            return;
        }

        // Update the primary values in the data record.
        ContentValues values = new ContentValues(1);
        values.put(Data.IS_SUPER_PRIMARY, 0);
        values.put(Data.IS_PRIMARY, 0);

        getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), values, null, null);
    }

    /**
     * Creates an intent that can be sent to this service to delete a contact.
     */
    public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
        return serviceIntent;
    }

    /**
     * Creates an intent that can be sent to this service to delete multiple contacts.
     */
    public static Intent createDeleteMultipleContactsIntent(Context context, long[] contactIds,
            final String[] names) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
        serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
        return serviceIntent;
    }

    private void deleteContact(Intent intent) {
        Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
        if (contactUri == null) {
            Log.e(TAG, "Invalid arguments for deleteContact request");
            return;
        }

        getContentResolver().delete(contactUri, null, null);
    }

    private void deleteMultipleContacts(Intent intent) {
        final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
        if (contactIds == null) {
            Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
            return;
        }
        for (long contactId : contactIds) {
            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
            getContentResolver().delete(contactUri, null, null);
        }
        final String[] names = intent.getStringArrayExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
        final String deleteToastMessage;
        if (contactIds.length != names.length || names.length == 0) {
            deleteToastMessage = getResources().getQuantityString(R.plurals.contacts_deleted_toast,
                    contactIds.length);
        } else if (names.length == 1) {
            deleteToastMessage = getResources().getString(R.string.contacts_deleted_one_named_toast, names);
        } else if (names.length == 2) {
            deleteToastMessage = getResources().getString(R.string.contacts_deleted_two_named_toast, names);
        } else {
            deleteToastMessage = getResources().getString(R.string.contacts_deleted_many_named_toast, names);
        }

        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG).show();
            }
        });
    }

    /**
     * Creates an intent that can be sent to this service to split a contact into it's constituent
     * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
     * they may be re-merged by the auto-aggregator.
     */
    public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
            ResultReceiver receiver) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
        return serviceIntent;
    }

    /**
     * Creates an intent that can be sent to this service to split a contact into it's constituent
     * pieces. This will explicitly set the raw contact ids to
     * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
     */
    public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
        serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
        return serviceIntent;
    }

    private void splitContact(Intent intent) {
        final long rawContactIds[][] = (long[][]) intent.getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
        final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
        if (rawContactIds == null) {
            Log.e(TAG, "Invalid argument for splitContact request");
            if (receiver != null) {
                receiver.send(BAD_ARGUMENTS, new Bundle());
            }
            return;
        }
        final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
        final ContentResolver resolver = getContentResolver();
        final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
        for (int i = 0; i < rawContactIds.length; i++) {
            for (int j = 0; j < rawContactIds.length; j++) {
                if (i != j) {
                    if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j], hardSplit)) {
                        if (receiver != null) {
                            receiver.send(CP2_ERROR, new Bundle());
                            return;
                        }
                    }
                }
            }
        }
        if (operations.size() > 0 && !applyOperations(resolver, operations)) {
            if (receiver != null) {
                receiver.send(CP2_ERROR, new Bundle());
            }
            return;
        }
        LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
        if (receiver != null) {
            receiver.send(CONTACTS_SPLIT, new Bundle());
        } else {
            showToast(R.string.contactUnlinkedToast);
        }
    }

    /**
     * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
     * and {@param rawContactIds2} to {@param operations}.
     * @return false if an error occurred, true otherwise.
     */
    private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations, long[] rawContactIds1,
            long[] rawContactIds2, boolean hardSplit) {
        if (rawContactIds1 == null || rawContactIds2 == null) {
            Log.e(TAG, "Invalid arguments for splitContact request");
            return false;
        }
        // For each pair of raw contacts, insert an aggregation exception
        final ContentResolver resolver = getContentResolver();
        // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
        final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
        for (int i = 0; i < rawContactIds1.length; i++) {
            for (int j = 0; j < rawContactIds2.length; j++) {
                buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
                // Before we get to 500 we need to flush the operations list
                if (operations.size() > 0 && operations.size() % batchSize == 0) {
                    if (!applyOperations(resolver, operations)) {
                        return false;
                    }
                    operations.clear();
                }
            }
        }
        return true;
    }

    /**
     * Creates an intent that can be sent to this service to join two contacts.
     * The resulting contact uses the name from {@param contactId1} if possible.
     */
    public static Intent createJoinContactsIntent(Context context, long contactId1, long contactId2,
            Class<? extends Activity> callbackActivity, String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);

        // Callback intent will be invoked by the service once the contacts are joined.
        Intent callbackIntent = new Intent(context, callbackActivity);
        callbackIntent.setAction(callbackAction);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);

        return serviceIntent;
    }

    /**
     * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
     * No special attention is paid to where the resulting contact's name is taken from.
     */
    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
            ResultReceiver receiver) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
        serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
        serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
        return serviceIntent;
    }

    /**
     * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
     * No special attention is paid to where the resulting contact's name is taken from.
     */
    public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
        return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
    }

    private interface JoinContactQuery {
        String[] PROJECTION = { RawContacts._ID, RawContacts.CONTACT_ID, RawContacts.DISPLAY_NAME_SOURCE, };

        int _ID = 0;
        int CONTACT_ID = 1;
        int DISPLAY_NAME_SOURCE = 2;
    }

    private interface ContactEntityQuery {
        String[] PROJECTION = { Contacts.Entity.DATA_ID, Contacts.Entity.CONTACT_ID,
                Contacts.Entity.IS_SUPER_PRIMARY, };
        String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" + " AND "
                + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME + " AND " + StructuredName.DISPLAY_NAME
                + " IS NOT NULL " + " AND " + StructuredName.DISPLAY_NAME + " != '' ";

        int DATA_ID = 0;
        int CONTACT_ID = 1;
        int IS_SUPER_PRIMARY = 2;
    }

    private void joinSeveralContacts(Intent intent) {
        final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);

        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);

        // Load raw contact IDs for all contacts involved.
        final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
        final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
        if (rawContactIds == null) {
            Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
            if (receiver != null) {
                receiver.send(BAD_ARGUMENTS, new Bundle());
            }
            return;
        }

        // For each pair of raw contacts, insert an aggregation exception
        final ContentResolver resolver = getContentResolver();
        // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
        final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
        final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
        for (int i = 0; i < rawContactIds.length; i++) {
            for (int j = 0; j < rawContactIds.length; j++) {
                if (i != j) {
                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
                }
                // Before we get to 500 we need to flush the operations list
                if (operations.size() > 0 && operations.size() % batchSize == 0) {
                    if (!applyOperations(resolver, operations)) {
                        if (receiver != null) {
                            receiver.send(CP2_ERROR, new Bundle());
                        }
                        return;
                    }
                    operations.clear();
                }
            }
        }
        if (operations.size() > 0 && !applyOperations(resolver, operations)) {
            if (receiver != null) {
                receiver.send(CP2_ERROR, new Bundle());
            }
            return;
        }

        final String name = queryNameOfLinkedContacts(contactIds);
        if (name != null) {
            if (receiver != null) {
                final Bundle result = new Bundle();
                result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
                result.putString(EXTRA_DISPLAY_NAME, name);
                receiver.send(CONTACTS_LINKED, result);
            } else {
                if (TextUtils.isEmpty(name)) {
                    showToast(R.string.contactsJoinedMessage);
                } else {
                    showToast(R.string.contactsJoinedNamedMessage, name);
                }
            }
            LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
        } else {
            if (receiver != null) {
                receiver.send(CP2_ERROR, new Bundle());
            }
            showToast(R.string.contactJoinErrorToast);
        }
    }

    /** Get the display name of the top-level contact after the contacts have been linked. */
    private String queryNameOfLinkedContacts(long[] contactIds) {
        final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
        final String[] whereArgs = new String[contactIds.length];
        for (int i = 0; i < contactIds.length; i++) {
            whereArgs[i] = String.valueOf(contactIds[i]);
            whereBuilder.append("?,");
        }
        whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
        final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
                new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_ALTERNATIVE },
                whereBuilder.toString(), whereArgs, null);

        String name = null;
        String nameAlt = null;
        long contactId = 0;
        try {
            if (cursor.moveToFirst()) {
                contactId = cursor.getLong(0);
                name = cursor.getString(1);
                nameAlt = cursor.getString(2);
            }
            while (cursor.moveToNext()) {
                if (cursor.getLong(0) != contactId) {
                    return null;
                }
            }

            final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
                    new ContactsPreferences(getApplicationContext()));
            return formattedName == null ? "" : formattedName;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /** Returns true if the batch was successfully applied and false otherwise. */
    private boolean applyOperations(ContentResolver resolver, ArrayList<ContentProviderOperation> operations) {
        try {
            final ContentProviderResult[] result = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
            for (int i = 0; i < result.length; ++i) {
                // if no rows were modified in the operation then we count it as fail.
                if (result[i].count < 0) {
                    throw new OperationApplicationException();
                }
            }
            return true;
        } catch (RemoteException | OperationApplicationException e) {
            FeedbackHelper.sendFeedback(this, TAG, "Failed to apply aggregation exception batch", e);
            showToast(R.string.contactSavedErrorToast);
            return false;
        }
    }

    private void joinContacts(Intent intent) {
        long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
        long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);

        // Load raw contact IDs for all raw contacts involved - currently edited and selected
        // in the join UIs.
        long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
        if (rawContactIds == null) {
            Log.e(TAG, "Invalid arguments for joinContacts request");
            return;
        }

        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();

        // For each pair of raw contacts, insert an aggregation exception
        for (int i = 0; i < rawContactIds.length; i++) {
            for (int j = 0; j < rawContactIds.length; j++) {
                if (i != j) {
                    buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
                }
            }
        }

        final ContentResolver resolver = getContentResolver();

        // Use the name for contactId1 as the name for the newly aggregated contact.
        final Uri contactId1Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
        final Uri entityUri = Uri.withAppendedPath(contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
        Cursor c = resolver.query(entityUri, ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null,
                null);
        if (c == null) {
            Log.e(TAG, "Unable to open Contacts DB cursor");
            showToast(R.string.contactSavedErrorToast);
            return;
        }
        long dataIdToAddSuperPrimary = -1;
        try {
            if (c.moveToFirst()) {
                dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
            }
        } finally {
            c.close();
        }

        // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
        // display name does not change as a result of the join.
        if (dataIdToAddSuperPrimary != -1) {
            Builder builder = ContentProviderOperation
                    .newUpdate(ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
            builder.withValue(Data.IS_SUPER_PRIMARY, 1);
            builder.withValue(Data.IS_PRIMARY, 1);
            operations.add(builder.build());
        }

        // Apply all aggregation exceptions as one batch
        final boolean success = applyOperations(resolver, operations);

        final String name = queryNameOfLinkedContacts(new long[] { contactId1, contactId2 });
        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        if (success && name != null) {
            if (TextUtils.isEmpty(name)) {
                showToast(R.string.contactsJoinedMessage);
            } else {
                showToast(R.string.contactsJoinedNamedMessage, name);
            }
            Uri uri = RawContacts.getContactLookupUri(resolver,
                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
            callbackIntent.setData(uri);
            LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
        }
        deliverCallback(callbackIntent);
    }

    /**
     * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
     * array of the return value holds an array of raw contact ids for one contactId.
     * @param contactIds
     * @return
     */
    private long[][] getSeparatedRawContactIds(long[] contactIds) {
        final long[][] rawContactIds = new long[contactIds.length][];
        for (int i = 0; i < contactIds.length; i++) {
            rawContactIds[i] = getRawContactIds(contactIds[i]);
        }
        return rawContactIds;
    }

    /**
     * Gets the raw contact ids associated with {@param contactId}.
     * @param contactId
     * @return Array of raw contact ids.
     */
    private long[] getRawContactIds(long contactId) {
        final ContentResolver resolver = getContentResolver();
        long rawContactIds[];

        final StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append(RawContacts.CONTACT_ID).append("=").append(String.valueOf(contactId));

        final Cursor c = resolver.query(RawContacts.CONTENT_URI, JoinContactQuery.PROJECTION,
                queryBuilder.toString(), null, null);
        if (c == null) {
            Log.e(TAG, "Unable to open Contacts DB cursor");
            return null;
        }
        try {
            rawContactIds = new long[c.getCount()];
            for (int i = 0; i < rawContactIds.length; i++) {
                c.moveToPosition(i);
                final long rawContactId = c.getLong(JoinContactQuery._ID);
                rawContactIds[i] = rawContactId;
            }
        } finally {
            c.close();
        }
        return rawContactIds;
    }

    private long[] getRawContactIdsForAggregation(long[] contactIds) {
        if (contactIds == null) {
            return null;
        }

        final ContentResolver resolver = getContentResolver();

        final StringBuilder queryBuilder = new StringBuilder();
        final String stringContactIds[] = new String[contactIds.length];
        for (int i = 0; i < contactIds.length; i++) {
            queryBuilder.append(RawContacts.CONTACT_ID + "=?");
            stringContactIds[i] = String.valueOf(contactIds[i]);
            if (contactIds[i] == -1) {
                return null;
            }
            if (i == contactIds.length - 1) {
                break;
            }
            queryBuilder.append(" OR ");
        }

        final Cursor c = resolver.query(RawContacts.CONTENT_URI, JoinContactQuery.PROJECTION,
                queryBuilder.toString(), stringContactIds, null);
        if (c == null) {
            Log.e(TAG, "Unable to open Contacts DB cursor");
            showToast(R.string.contactSavedErrorToast);
            return null;
        }
        long rawContactIds[];
        try {
            if (c.getCount() < 2) {
                Log.e(TAG, "Not enough raw contacts to aggregate together.");
                return null;
            }
            rawContactIds = new long[c.getCount()];
            for (int i = 0; i < rawContactIds.length; i++) {
                c.moveToPosition(i);
                long rawContactId = c.getLong(JoinContactQuery._ID);
                rawContactIds[i] = rawContactId;
            }
        } finally {
            c.close();
        }
        return rawContactIds;
    }

    private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
        return getRawContactIdsForAggregation(new long[] { contactId1, contactId2 });
    }

    /**
     * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
     */
    private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1,
            long rawContactId2) {
        Builder builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
        operations.add(builder.build());
    }

    /**
     * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
     * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
     * requested.
     */
    private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations, long rawContactId1,
            long rawContactId2, boolean hardSplit) {
        final Builder builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
        builder.withValue(AggregationExceptions.TYPE,
                hardSplit ? AggregationExceptions.TYPE_KEEP_SEPARATE : AggregationExceptions.TYPE_AUTOMATIC);
        builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
        operations.add(builder.build());
    }

    /**
     * Returns an intent that can start this service and cause it to sleep for the specified time.
     *
     * This exists purely for debugging and manual testing. Since this service uses a single thread
     * it is useful to have a way to test behavior when work is queued up and most of the other
     * operations complete too quickly to simulate that under normal conditions.
     */
    public static Intent createSleepIntent(Context context, long millis) {
        return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP).putExtra(EXTRA_SLEEP_DURATION,
                millis);
    }

    private void sleepForDebugging(Intent intent) {
        long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "sleeping for " + duration + "ms");
        }
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "finished sleeping");
        }
    }

    /**
     * Shows a toast on the UI thread by formatting messageId using args.
     * @param messageId id of message string
     * @param args args to format string
     */
    private void showToast(final int messageId, final Object... args) {
        final String message = getResources().getString(messageId, args);
        mMainHandler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
            }
        });
    }

    /**
     * Shows a toast on the UI thread.
     */
    private void showToast(final int message) {
        mMainHandler.post(new Runnable() {

            @Override
            public void run() {
                Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
            }
        });
    }

    private void deliverCallback(final Intent callbackIntent) {
        mMainHandler.post(new Runnable() {

            @Override
            public void run() {
                deliverCallbackOnUiThread(callbackIntent);
            }
        });
    }

    void deliverCallbackOnUiThread(final Intent callbackIntent) {
        // TODO: this assumes that if there are multiple instances of the same
        // activity registered, the last one registered is the one waiting for
        // the callback. Validity of this assumption needs to be verified.
        for (Listener listener : sListeners) {
            if (callbackIntent.getComponent().equals(((Activity) listener).getIntent().getComponent())) {
                listener.onServiceCompleted(callbackIntent);
                return;
            }
        }
    }

    public interface GroupsDao {
        Uri create(String title, AccountWithDataSet account);

        int delete(Uri groupUri);

        Bundle captureDeletionUndoData(Uri groupUri);

        Uri undoDeletion(Bundle undoData);
    }

    public static class GroupsDaoImpl implements GroupsDao {
        public static final String KEY_GROUP_DATA = "groupData";
        public static final String KEY_GROUP_MEMBERS = "groupMemberIds";

        private static final String TAG = "GroupsDao";
        private final Context context;
        private final ContentResolver contentResolver;

        public GroupsDaoImpl(Context context) {
            this(context, context.getContentResolver());
        }

        public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
            this.context = context;
            this.contentResolver = contentResolver;
        }

        public Bundle captureDeletionUndoData(Uri groupUri) {
            final long groupId = ContentUris.parseId(groupUri);
            final Bundle result = new Bundle();

            final Cursor cursor = contentResolver.query(groupUri,
                    new String[] { Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE, Groups.ACCOUNT_TYPE,
                            Groups.ACCOUNT_NAME, Groups.DATA_SET, Groups.SHOULD_SYNC },
                    Groups.DELETED + "=?", new String[] { "0" }, null);
            try {
                if (cursor.moveToFirst()) {
                    final ContentValues groupValues = new ContentValues();
                    DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
                    result.putParcelable(KEY_GROUP_DATA, groupValues);
                } else {
                    // Group doesn't exist.
                    return result;
                }
            } finally {
                cursor.close();
            }

            final Cursor membersCursor = contentResolver.query(Data.CONTENT_URI,
                    new String[] { Data.RAW_CONTACT_ID },
                    Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
                    new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
            final long[] memberIds = new long[membersCursor.getCount()];
            int i = 0;
            while (membersCursor.moveToNext()) {
                memberIds[i++] = membersCursor.getLong(0);
            }
            result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
            return result;
        }

        public Uri undoDeletion(Bundle deletedGroupData) {
            final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
            if (groupData == null) {
                return null;
            }
            final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
            final long groupId = ContentUris.parseId(groupUri);

            final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
            if (memberIds == null) {
                return groupUri;
            }
            final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
            for (int i = 0; i < memberIds.length; i++) {
                memberInsertions[i] = new ContentValues();
                memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
                memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
                memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
            }
            final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
            if (inserted != memberIds.length) {
                Log.e(TAG, "Could not recover some members for group deletion undo");
            }

            return groupUri;
        }

        public Uri create(String title, AccountWithDataSet account) {
            final ContentValues values = new ContentValues();
            values.put(Groups.TITLE, title);
            values.put(Groups.ACCOUNT_NAME, account.name);
            values.put(Groups.ACCOUNT_TYPE, account.type);
            values.put(Groups.DATA_SET, account.dataSet);
            return contentResolver.insert(Groups.CONTENT_URI, values);
        }

        public int delete(Uri groupUri) {
            return contentResolver.delete(groupUri, null, null);
        }
    }

    /**
     * Keeps track of which operations have been requested but have not yet finished for this
     * service.
     */
    public static class State {
        private final CopyOnWriteArrayList<Intent> mPending;

        public State() {
            mPending = new CopyOnWriteArrayList<>();
        }

        public State(Collection<Intent> pendingActions) {
            mPending = new CopyOnWriteArrayList<>(pendingActions);
        }

        public boolean isIdle() {
            return mPending.isEmpty();
        }

        public Intent getCurrentIntent() {
            return mPending.isEmpty() ? null : mPending.get(0);
        }

        /**
         * Returns the first intent requested that has the specified action or null if no intent
         * with that action has been requested.
         */
        public Intent getNextIntentWithAction(String action) {
            for (Intent intent : mPending) {
                if (action.equals(intent.getAction())) {
                    return intent;
                }
            }
            return null;
        }

        public boolean isActionPending(String action) {
            return getNextIntentWithAction(action) != null;
        }

        private void onFinish(Intent intent) {
            if (mPending.isEmpty()) {
                return;
            }
            final String action = mPending.get(0).getAction();
            if (action.equals(intent.getAction())) {
                mPending.remove(0);
            }
        }

        private void onStart(Intent intent) {
            if (intent.getAction() == null) {
                return;
            }
            mPending.add(intent);
        }
    }
}