Moving contact saving to the service

Bug: 3220304
Bug: 3452739
Change-Id: If4124096a24e5dd302feb5338efaaa8398b2cb6b
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index c07dd68..f7ede90 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -16,7 +16,9 @@
 
 package com.android.contacts;
 
-import com.android.contacts.R;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
 
@@ -35,6 +37,7 @@
 import android.net.Uri;
 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;
@@ -63,7 +66,9 @@
     public static final String EXTRA_CONTENT_VALUES = "contentValues";
     public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
 
-    public static final String EXTRA_OPERATIONS = "Operations";
+    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 ACTION_CREATE_GROUP = "createGroup";
     public static final String ACTION_RENAME_GROUP = "renameGroup";
@@ -105,16 +110,30 @@
         Data.DATA15
     );
 
+    private static final int PERSIST_TRIES = 3;
+
     public ContactSaveService() {
         super(TAG);
         setIntentRedelivery(true);
     }
 
     @Override
+    public Object getSystemService(String name) {
+        Object service = super.getSystemService(name);
+        if (service != null) {
+            return service;
+        }
+
+        return getApplicationContext().getSystemService(name);
+    }
+
+    @Override
     protected void onHandleIntent(Intent intent) {
         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)) {
@@ -199,6 +218,121 @@
     }
 
     /**
+     * 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 createSaveContactIntent(Context context, EntityDeltaList state,
+            String saveModeExtraKey, int saveMode, Class<?> callbackActivity,
+            String callbackAction) {
+        Intent serviceIntent = new Intent(
+                context, ContactSaveService.class);
+        serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
+        serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
+
+        // 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);
+        callbackIntent.setAction(callbackAction);
+        callbackIntent.setFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+        return serviceIntent;
+    }
+
+    private void saveContact(Intent intent) {
+        EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
+        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+
+        // Trim any empty fields, and RawContacts, before persisting
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+        EntityModifier.trimEmpty(state, accountTypes);
+
+        Uri lookupUri = null;
+
+        final ContentResolver resolver = getContentResolver();
+
+        // Attempt to persist changes
+        int tries = 0;
+        while (tries++ < PERSIST_TRIES) {
+            try {
+                // Build operations and try applying
+                final ArrayList<ContentProviderOperation> diff = state.buildDiff();
+                ContentProviderResult[] results = null;
+                if (!diff.isEmpty()) {
+                    results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+                }
+
+                final long rawContactId = getRawContactId(state, diff, results);
+                if (rawContactId == -1) {
+                    throw new IllegalStateException("Could not determine RawContact ID after save");
+                }
+                final Uri rawContactUri = ContentUris.withAppendedId(
+                        RawContacts.CONTENT_URI, rawContactId);
+                lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
+                Log.v(TAG, "Saved contact. New URI: " + lookupUri);
+                break;
+
+            } catch (RemoteException e) {
+                // Something went wrong, bail without success
+                Log.e(TAG, "Problem persisting user edits", e);
+                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");
+                }
+
+                final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
+                        sb.toString(), null, null);
+                state = EntityDeltaList.mergeAfter(newState, state);
+            }
+        }
+
+        callbackIntent.setData(lookupUri);
+
+        startActivity(callbackIntent);
+    }
+
+    private long getRawContactId(EntityDeltaList state,
+            final ArrayList<ContentProviderOperation> diff,
+            final ContentProviderResult[] results) {
+        long rawContactId = state.findRawContactId();
+        if (rawContactId != -1) {
+            return rawContactId;
+        }
+
+        final int diffSize = diff.size();
+        for (int i = 0; i < diffSize; i++) {
+            ContentProviderOperation operation = diff.get(i);
+            if (operation.getType() == ContentProviderOperation.TYPE_INSERT
+                    && operation.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.
      */
     public static Intent createNewGroupIntent(Context context, Account account, String label,
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index 00e0ab0..3452edc 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -50,6 +50,7 @@
     private static final String TAG = "ContactEditorActivity";
 
     public static final String ACTION_JOIN_COMPLETED = "joinCompleted";
+    public static final String ACTION_SAVE_COMPLETED = "saveCompleted";
 
     private ContactEditorFragment mFragment;
     private Button mDoneButton;
@@ -113,6 +114,10 @@
         String action = intent.getAction();
         if (Intent.ACTION_EDIT.equals(action)) {
             mFragment.setIntentExtras(intent.getExtras());
+        } else if (ACTION_SAVE_COMPLETED.equals(action)) {
+            mFragment.onSaveCompleted(true,
+                    intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE),
+                    intent.getData());
         } else if (ACTION_JOIN_COMPLETED.equals(action)) {
             mFragment.onJoinCompleted(intent.getData());
         }
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index b117d77..f8415d2 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -31,8 +31,6 @@
 import com.android.contacts.model.EntityDeltaList;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.GoogleAccountType;
-import com.android.contacts.util.EmptyService;
-import com.android.contacts.util.WeakAsyncTask;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -43,9 +41,6 @@
 import android.app.LoaderManager;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -54,7 +49,6 @@
 import android.content.Entity;
 import android.content.Intent;
 import android.content.Loader;
-import android.content.OperationApplicationException;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
@@ -62,7 +56,6 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -115,11 +108,12 @@
     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
     private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile";
-    private static final String KEY_QUERY_SELECTION = "queryselection";
     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
     private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions";
     private static final String KEY_ENABLED = "enabled";
 
+    public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
+
     /**
      * An intent extra that forces the editor to add the edited contact
      * to the default group (e.g. "My Contacts").
@@ -215,8 +209,6 @@
     private Bundle mIntentExtras;
     private Listener mListener;
 
-    private String mQuerySelection;
-
     private long mContactIdForJoin;
 
     private LinearLayout mContent;
@@ -316,6 +308,8 @@
                     // Load Accounts async so that we can present them
                     selectAccountAndCreateContact();
                 }
+            } else if (ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(mAction)) {
+                // do nothing
             } else throw new IllegalArgumentException("Unknown Action String " + mAction +
                     ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
         }
@@ -364,7 +358,6 @@
             if (fileName != null) {
                 mCurrentPhotoFile = new File(fileName);
             }
-            mQuerySelection = savedState.getString(KEY_QUERY_SELECTION);
             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
             mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS);
             mEnabled = savedState.getBoolean(KEY_ENABLED);
@@ -409,19 +402,7 @@
     private void bindEditorsForExistingContact(ContactLoader.Result data) {
         setEnabled(true);
 
-        // Build Filter mQuerySelection
-        final ArrayList<Entity> entities = data.getEntities();
-        final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
-        final int count = entities.size();
-        for (int i = 0; i < count; i++) {
-            if (i > 0) {
-                sb.append(',');
-            }
-            sb.append(entities.get(i).getEntityValues().get(RawContacts._ID));
-        }
-        sb.append(")");
-        mQuerySelection = sb.toString();
-        mState = EntityDeltaList.fromIterator(entities.iterator());
+        mState = EntityDeltaList.fromIterator(data.getEntities().iterator());
         setIntentExtras(mIntentExtras);
         mIntentExtras = null;
 
@@ -671,14 +652,12 @@
 
         // If we just started creating a new contact and haven't added any data, it's too
         // early to do a join
-        if (mState.size() == 1 && mState.get(0).isContactInsert()) {
-            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-            EntityModifier.trimEmpty(mState, accountTypes);
-            if (mState.buildDiff().isEmpty()) {
-                Toast.makeText(getActivity(), R.string.toast_join_with_empty_contact,
-                                Toast.LENGTH_LONG).show();
-                return true;
-            }
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
+        if (mState.size() == 1 && mState.get(0).isContactInsert()
+                && !EntityModifier.hasChanges(mState, accountTypes)) {
+            Toast.makeText(getActivity(), R.string.toast_join_with_empty_contact,
+                            Toast.LENGTH_LONG).show();
+            return true;
         }
 
         return save(SaveMode.JOIN);
@@ -772,24 +751,24 @@
         }
 
         // If we are about to close the editor - there is no need to refresh the data
-        if (saveMode == SaveMode.CLOSE) {
+        if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) {
             getLoaderManager().destroyLoader(LOADER_DATA);
         }
 
         mStatus = Status.SAVING;
 
-        // Trim any empty fields, and RawContacts, before persisting
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        EntityModifier.trimEmpty(mState, accountTypes);
-
-        if (mState.buildDiff().isEmpty()) {
-            onSaveCompleted(true, saveMode, mLookupUri);
+        if (!EntityModifier.hasChanges(mState, accountTypes)) {
+            onSaveCompleted(false, saveMode, mLookupUri);
             return true;
         }
 
-        final PersistTask task = new PersistTask(this, saveMode);
-        task.execute(mState);
+        setEnabled(false);
 
+        Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState,
+                SAVE_MODE_EXTRA_KEY, saveMode, getActivity().getClass(),
+                ContactEditorActivity.ACTION_SAVE_COMPLETED);
+        getActivity().startService(intent);
         return true;
     }
 
@@ -837,11 +816,21 @@
     }
 
     public void onJoinCompleted(Uri uri) {
-        onSaveCompleted(uri != null, SaveMode.RELOAD, uri);
+        onSaveCompleted(false, SaveMode.RELOAD, uri);
     }
 
-    public void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
-        Log.d(TAG, "onSaveCompleted(" + success + ", " + saveMode + ", " + contactLookupUri);
+    public void onSaveCompleted(boolean hadChanges, int saveMode, Uri contactLookupUri) {
+        boolean success = contactLookupUri != null;
+        Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + contactLookupUri);
+        if (hadChanges) {
+            if (success) {
+                if (saveMode != SaveMode.JOIN) {
+                    Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+                }
+            } else {
+                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
+            }
+        }
         switch (saveMode) {
             case SaveMode.CLOSE:
             case SaveMode.HOME:
@@ -877,6 +866,7 @@
                 if (mListener != null) mListener.onSaveFinished(resultCode, resultIntent,
                         saveMode == SaveMode.HOME);
                 break;
+
             case SaveMode.RELOAD:
             case SaveMode.JOIN:
                 if (success && contactLookupUri != null) {
@@ -893,14 +883,14 @@
                     }
                 }
                 break;
+
             case SaveMode.SPLIT:
-                setEnabled(true);
+                mStatus = Status.CLOSING;
                 if (mListener != null) {
                     mListener.onContactSplit(contactLookupUri);
                 } else {
                     Log.d(TAG, "No listener registered, can not call onSplitFinished");
                 }
-                mStatus = Status.EDITING;
                 break;
         }
     }
@@ -1430,133 +1420,6 @@
         return rect;
     }
 
-    // TODO: There has to be a nicer way than this WeakAsyncTask...? Maybe call a service?
-    /**
-     * Background task for persisting edited contact data, using the changes
-     * defined by a set of {@link EntityDelta}. This task starts
-     * {@link EmptyService} to make sure the background thread can finish
-     * persisting in cases where the system wants to reclaim our process.
-     */
-    public static class PersistTask extends
-            WeakAsyncTask<EntityDeltaList, Void, Integer, ContactEditorFragment> {
-        private static final int PERSIST_TRIES = 3;
-
-        private static final int RESULT_UNCHANGED = 0;
-        private static final int RESULT_SUCCESS = 1;
-        private static final int RESULT_FAILURE = 2;
-
-        private final Context mContext;
-
-        private int mSaveMode;
-        private Uri mContactLookupUri = null;
-
-        public PersistTask(ContactEditorFragment target, int saveMode) {
-            super(target);
-            mSaveMode = saveMode;
-            mContext = target.mContext;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        protected void onPreExecute(ContactEditorFragment target) {
-            target.setEnabled(false);
-
-            // Before starting this task, start an empty service to protect our
-            // process from being reclaimed by the system.
-            mContext.startService(new Intent(mContext, EmptyService.class));
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        protected Integer doInBackground(ContactEditorFragment target, EntityDeltaList... params) {
-            final ContentResolver resolver = mContext.getContentResolver();
-
-            EntityDeltaList state = params[0];
-
-            // Attempt to persist changes
-            int tries = 0;
-            Integer result = RESULT_FAILURE;
-            while (tries++ < PERSIST_TRIES) {
-                try {
-                    // Build operations and try applying
-                    final ArrayList<ContentProviderOperation> diff = state.buildDiff();
-                    ContentProviderResult[] results = null;
-                    if (!diff.isEmpty()) {
-                        results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
-                    }
-
-                    final long rawContactId = getRawContactId(state, diff, results);
-                    if (rawContactId != -1) {
-                        final Uri rawContactUri = ContentUris.withAppendedId(
-                                RawContacts.CONTENT_URI, rawContactId);
-
-                        // convert the raw contact URI to a contact URI
-                        mContactLookupUri = RawContacts.getContactLookupUri(resolver,
-                                rawContactUri);
-                        Log.d(TAG, "Looked up RawContact Uri " + rawContactUri +
-                                " into ContactLookupUri " + mContactLookupUri);
-                    } else {
-                        Log.w(TAG, "Could not determine RawContact ID after save");
-                    }
-                    result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
-                    break;
-
-                } catch (RemoteException e) {
-                    // Something went wrong, bail without success
-                    Log.e(TAG, "Problem persisting user edits", e);
-                    break;
-
-                } catch (OperationApplicationException e) {
-                    // Version consistency failed, re-parent change and try again
-                    Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
-                    final EntityDeltaList newState = EntityDeltaList.fromQuery(resolver,
-                            target.mQuerySelection, null, null);
-                    state = EntityDeltaList.mergeAfter(newState, state);
-                }
-            }
-
-            return result;
-        }
-
-        private long getRawContactId(EntityDeltaList state,
-                final ArrayList<ContentProviderOperation> diff,
-                final ContentProviderResult[] results) {
-            long rawContactId = state.findRawContactId();
-            if (rawContactId != -1) {
-                return rawContactId;
-            }
-
-
-            // we gotta do some searching for the id
-            final int diffSize = diff.size();
-            for (int i = 0; i < diffSize; i++) {
-                ContentProviderOperation operation = diff.get(i);
-                if (operation.getType() == ContentProviderOperation.TYPE_INSERT
-                        && operation.getUri().getEncodedPath().contains(
-                                RawContacts.CONTENT_URI.getEncodedPath())) {
-                    return ContentUris.parseId(results[i].uri);
-                }
-            }
-            return -1;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        protected void onPostExecute(ContactEditorFragment target, Integer result) {
-            Log.d(TAG, "onPostExecute(something," + result + "). mSaveMode=" + mSaveMode);
-            if (result == RESULT_SUCCESS && mSaveMode != SaveMode.JOIN) {
-                Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
-            } else if (result == RESULT_FAILURE) {
-                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
-            }
-
-            // Stop the service that was protecting us
-            mContext.stopService(new Intent(mContext, EmptyService.class));
-
-            target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
-        }
-    }
-
     @Override
     public void onSaveInstanceState(Bundle outState) {
         outState.putParcelable(KEY_URI, mLookupUri);
@@ -1572,7 +1435,6 @@
         if (mCurrentPhotoFile != null) {
             outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString());
         }
-        outState.putString(KEY_QUERY_SELECTION, mQuerySelection);
         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
         outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
         outState.putBoolean(KEY_ENABLED, mEnabled);
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/EntityDeltaList.java
index a998a37..9b7bdc6 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/EntityDeltaList.java
@@ -340,10 +340,18 @@
         mSplitRawContacts = true;
     }
 
+    public boolean isMarkedForSplitting() {
+        return mSplitRawContacts;
+    }
+
     public void setJoinWithRawContacts(long[] rawContactIds) {
         mJoinWithRawContactIds = rawContactIds;
     }
 
+    public boolean isMarkedForJoining() {
+        return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
+    }
+
     /** {@inheritDoc} */
     public int describeContents() {
         // Nothing special about this parcel
@@ -358,6 +366,7 @@
             dest.writeParcelable(delta, flags);
         }
         dest.writeLongArray(mJoinWithRawContactIds);
+        dest.writeInt(mSplitRawContacts ? 1 : 0);
     }
 
     @SuppressWarnings("unchecked")
@@ -368,6 +377,7 @@
             this.add(source.<EntityDelta> readParcelable(loader));
         }
         mJoinWithRawContactIds = source.createLongArray();
+        mSplitRawContacts = source.readInt() != 0;
     }
 
     public static final Parcelable.Creator<EntityDeltaList> CREATOR =
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index c58d813..4d4e7a8 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -344,7 +344,7 @@
         }
 
         final ValuesDelta child = ValuesDelta.fromAfter(after);
-	state.addEntry(child);
+        state.addEntry(child);
         return child;
     }
 
@@ -357,11 +357,26 @@
     public static void trimEmpty(EntityDeltaList set, AccountTypeManager accountTypes) {
         for (EntityDelta state : set) {
             final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-            final AccountType source = accountTypes.getAccountType(accountType);
-            trimEmpty(state, source);
+            final AccountType type = accountTypes.getAccountType(accountType);
+            trimEmpty(state, type);
         }
     }
 
+    public static boolean hasChanges(EntityDeltaList set, AccountTypeManager accountTypes) {
+        if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
+            return true;
+        }
+
+        for (EntityDelta state : set) {
+            final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+            final AccountType type = accountTypes.getAccountType(accountType);
+            if (hasChanges(state, type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Processing to trim any empty {@link ValuesDelta} rows from the given
      * {@link EntityDelta}, assuming the given {@link AccountType} dictates
@@ -406,6 +421,21 @@
         }
     }
 
+    private static boolean hasChanges(EntityDelta state, AccountType accountType) {
+        for (DataKind kind : accountType.getSortedDataKinds()) {
+            final String mimeType = kind.mimeType;
+            final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                if ((entry.isInsert() || entry.isUpdate()) && !isEmpty(entry, kind)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * Test if the given {@link ValuesDelta} would be considered "empty" in
      * terms of {@link DataKind#fieldList}.