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,