Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts;
     18 
     19 import android.app.Activity;
     20 import android.app.IntentService;
     21 import android.content.ContentProviderOperation;
     22 import android.content.ContentProviderOperation.Builder;
     23 import android.content.ContentProviderResult;
     24 import android.content.ContentResolver;
     25 import android.content.ContentUris;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.OperationApplicationException;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.os.Bundle;
     33 import android.os.Handler;
     34 import android.os.Looper;
     35 import android.os.Parcelable;
     36 import android.os.RemoteException;
     37 import android.provider.ContactsContract;
     38 import android.provider.ContactsContract.AggregationExceptions;
     39 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     40 import android.provider.ContactsContract.Contacts;
     41 import android.provider.ContactsContract.Data;
     42 import android.provider.ContactsContract.Groups;
     43 import android.provider.ContactsContract.PinnedPositions;
     44 import android.provider.ContactsContract.Profile;
     45 import android.provider.ContactsContract.RawContacts;
     46 import android.provider.ContactsContract.RawContactsEntity;
     47 import android.util.Log;
     48 import android.widget.Toast;
     49 
     50 import com.android.contacts.common.database.ContactUpdateUtils;
     51 import com.android.contacts.common.model.AccountTypeManager;
     52 import com.android.contacts.common.model.RawContactDelta;
     53 import com.android.contacts.common.model.RawContactDeltaList;
     54 import com.android.contacts.common.model.RawContactModifier;
     55 import com.android.contacts.common.model.account.AccountWithDataSet;
     56 import com.android.contacts.util.ContactPhotoUtils;
     57 
     58 import com.google.common.collect.Lists;
     59 import com.google.common.collect.Sets;
     60 
     61 import java.io.File;
     62 import java.io.FileInputStream;
     63 import java.io.FileOutputStream;
     64 import java.io.IOException;
     65 import java.io.InputStream;
     66 import java.util.ArrayList;
     67 import java.util.HashSet;
     68 import java.util.List;
     69 import java.util.concurrent.CopyOnWriteArrayList;
     70 
     71 /**
     72  * A service responsible for saving changes to the content provider.
     73  */
     74 public class ContactSaveService extends IntentService {
     75     private static final String TAG = "ContactSaveService";
     76 
     77     /** Set to true in order to view logs on content provider operations */
     78     private static final boolean DEBUG = false;
     79 
     80     public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
     81 
     82     public static final String EXTRA_ACCOUNT_NAME = "accountName";
     83     public static final String EXTRA_ACCOUNT_TYPE = "accountType";
     84     public static final String EXTRA_DATA_SET = "dataSet";
     85     public static final String EXTRA_CONTENT_VALUES = "contentValues";
     86     public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
     87 
     88     public static final String ACTION_SAVE_CONTACT = "saveContact";
     89     public static final String EXTRA_CONTACT_STATE = "state";
     90     public static final String EXTRA_SAVE_MODE = "saveMode";
     91     public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
     92     public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
     93     public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
     94 
     95     public static final String ACTION_CREATE_GROUP = "createGroup";
     96     public static final String ACTION_RENAME_GROUP = "renameGroup";
     97     public static final String ACTION_DELETE_GROUP = "deleteGroup";
     98     public static final String ACTION_UPDATE_GROUP = "updateGroup";
     99     public static final String EXTRA_GROUP_ID = "groupId";
    100     public static final String EXTRA_GROUP_LABEL = "groupLabel";
    101     public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
    102     public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
    103 
    104     public static final String ACTION_SET_STARRED = "setStarred";
    105     public static final String ACTION_DELETE_CONTACT = "delete";
    106     public static final String EXTRA_CONTACT_URI = "contactUri";
    107     public static final String EXTRA_STARRED_FLAG = "starred";
    108 
    109     public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
    110     public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
    111     public static final String EXTRA_DATA_ID = "dataId";
    112 
    113     public static final String ACTION_JOIN_CONTACTS = "joinContacts";
    114     public static final String EXTRA_CONTACT_ID1 = "contactId1";
    115     public static final String EXTRA_CONTACT_ID2 = "contactId2";
    116     public static final String EXTRA_CONTACT_WRITABLE = "contactWritable";
    117 
    118     public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
    119     public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
    120 
    121     public static final String ACTION_SET_RINGTONE = "setRingtone";
    122     public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
    123 
    124     private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
    125         Data.MIMETYPE,
    126         Data.IS_PRIMARY,
    127         Data.DATA1,
    128         Data.DATA2,
    129         Data.DATA3,
    130         Data.DATA4,
    131         Data.DATA5,
    132         Data.DATA6,
    133         Data.DATA7,
    134         Data.DATA8,
    135         Data.DATA9,
    136         Data.DATA10,
    137         Data.DATA11,
    138         Data.DATA12,
    139         Data.DATA13,
    140         Data.DATA14,
    141         Data.DATA15
    142     );
    143 
    144     private static final int PERSIST_TRIES = 3;
    145 
    146     public interface Listener {
    147         public void onServiceCompleted(Intent callbackIntent);
    148     }
    149 
    150     private static final CopyOnWriteArrayList<Listener> sListeners =
    151             new CopyOnWriteArrayList<Listener>();
    152 
    153     private Handler mMainHandler;
    154 
    155     public ContactSaveService() {
    156         super(TAG);
    157         setIntentRedelivery(true);
    158         mMainHandler = new Handler(Looper.getMainLooper());
    159     }
    160 
    161     public static void registerListener(Listener listener) {
    162         if (!(listener instanceof Activity)) {
    163             throw new ClassCastException("Only activities can be registered to"
    164                     + " receive callback from " + ContactSaveService.class.getName());
    165         }
    166         sListeners.add(0, listener);
    167     }
    168 
    169     public static void unregisterListener(Listener listener) {
    170         sListeners.remove(listener);
    171     }
    172 
    173     @Override
    174     public Object getSystemService(String name) {
    175         Object service = super.getSystemService(name);
    176         if (service != null) {
    177             return service;
    178         }
    179 
    180         return getApplicationContext().getSystemService(name);
    181     }
    182 
    183     @Override
    184     protected void onHandleIntent(Intent intent) {
    185         // Call an appropriate method. If we're sure it affects how incoming phone calls are
    186         // handled, then notify the fact to in-call screen.
    187         String action = intent.getAction();
    188         if (ACTION_NEW_RAW_CONTACT.equals(action)) {
    189             createRawContact(intent);
    190         } else if (ACTION_SAVE_CONTACT.equals(action)) {
    191             saveContact(intent);
    192         } else if (ACTION_CREATE_GROUP.equals(action)) {
    193             createGroup(intent);
    194         } else if (ACTION_RENAME_GROUP.equals(action)) {
    195             renameGroup(intent);
    196         } else if (ACTION_DELETE_GROUP.equals(action)) {
    197             deleteGroup(intent);
    198         } else if (ACTION_UPDATE_GROUP.equals(action)) {
    199             updateGroup(intent);
    200         } else if (ACTION_SET_STARRED.equals(action)) {
    201             setStarred(intent);
    202         } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
    203             setSuperPrimary(intent);
    204         } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
    205             clearPrimary(intent);
    206         } else if (ACTION_DELETE_CONTACT.equals(action)) {
    207             deleteContact(intent);
    208         } else if (ACTION_JOIN_CONTACTS.equals(action)) {
    209             joinContacts(intent);
    210         } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
    211             setSendToVoicemail(intent);
    212         } else if (ACTION_SET_RINGTONE.equals(action)) {
    213             setRingtone(intent);
    214         }
    215     }
    216 
    217     /**
    218      * Creates an intent that can be sent to this service to create a new raw contact
    219      * using data presented as a set of ContentValues.
    220      */
    221     public static Intent createNewRawContactIntent(Context context,
    222             ArrayList<ContentValues> values, AccountWithDataSet account,
    223             Class<? extends Activity> callbackActivity, String callbackAction) {
    224         Intent serviceIntent = new Intent(
    225                 context, ContactSaveService.class);
    226         serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
    227         if (account != null) {
    228             serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
    229             serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
    230             serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
    231         }
    232         serviceIntent.putParcelableArrayListExtra(
    233                 ContactSaveService.EXTRA_CONTENT_VALUES, values);
    234 
    235         // Callback intent will be invoked by the service once the new contact is
    236         // created.  The service will put the URI of the new contact as "data" on
    237         // the callback intent.
    238         Intent callbackIntent = new Intent(context, callbackActivity);
    239         callbackIntent.setAction(callbackAction);
    240         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    241         return serviceIntent;
    242     }
    243 
    244     private void createRawContact(Intent intent) {
    245         String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
    246         String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
    247         String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
    248         List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
    249         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    250 
    251         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
    252         operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
    253                 .withValue(RawContacts.ACCOUNT_NAME, accountName)
    254                 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
    255                 .withValue(RawContacts.DATA_SET, dataSet)
    256                 .build());
    257 
    258         int size = valueList.size();
    259         for (int i = 0; i < size; i++) {
    260             ContentValues values = valueList.get(i);
    261             values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
    262             operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
    263                     .withValueBackReference(Data.RAW_CONTACT_ID, 0)
    264                     .withValues(values)
    265                     .build());
    266         }
    267 
    268         ContentResolver resolver = getContentResolver();
    269         ContentProviderResult[] results;
    270         try {
    271             results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    272         } catch (Exception e) {
    273             throw new RuntimeException("Failed to store new contact", e);
    274         }
    275 
    276         Uri rawContactUri = results[0].uri;
    277         callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
    278 
    279         deliverCallback(callbackIntent);
    280     }
    281 
    282     /**
    283      * Creates an intent that can be sent to this service to create a new raw contact
    284      * using data presented as a set of ContentValues.
    285      * This variant is more convenient to use when there is only one photo that can
    286      * possibly be updated, as in the Contact Details screen.
    287      * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
    288      * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
    289      */
    290     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
    291             String saveModeExtraKey, int saveMode, boolean isProfile,
    292             Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
    293             Uri updatedPhotoPath) {
    294         Bundle bundle = new Bundle();
    295         bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
    296         return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
    297                 callbackActivity, callbackAction, bundle);
    298     }
    299 
    300     /**
    301      * Creates an intent that can be sent to this service to create a new raw contact
    302      * using data presented as a set of ContentValues.
    303      * This variant is used when multiple contacts' photos may be updated, as in the
    304      * Contact Editor.
    305      * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
    306      */
    307     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
    308             String saveModeExtraKey, int saveMode, boolean isProfile,
    309             Class<? extends Activity> callbackActivity, String callbackAction,
    310             Bundle updatedPhotos) {
    311         Intent serviceIntent = new Intent(
    312                 context, ContactSaveService.class);
    313         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
    314         serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
    315         serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
    316         if (updatedPhotos != null) {
    317             serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
    318         }
    319 
    320         if (callbackActivity != null) {
    321             // Callback intent will be invoked by the service once the contact is
    322             // saved.  The service will put the URI of the new contact as "data" on
    323             // the callback intent.
    324             Intent callbackIntent = new Intent(context, callbackActivity);
    325             callbackIntent.putExtra(saveModeExtraKey, saveMode);
    326             callbackIntent.setAction(callbackAction);
    327             serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    328         }
    329         return serviceIntent;
    330     }
    331 
    332     private void saveContact(Intent intent) {
    333         RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
    334         boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
    335         Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
    336 
    337         // Trim any empty fields, and RawContacts, before persisting
    338         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
    339         RawContactModifier.trimEmpty(state, accountTypes);
    340 
    341         Uri lookupUri = null;
    342 
    343         final ContentResolver resolver = getContentResolver();
    344         boolean succeeded = false;
    345 
    346         // Keep track of the id of a newly raw-contact (if any... there can be at most one).
    347         long insertedRawContactId = -1;
    348 
    349         // Attempt to persist changes
    350         int tries = 0;
    351         while (tries++ < PERSIST_TRIES) {
    352             try {
    353                 // Build operations and try applying
    354                 final ArrayList<ContentProviderOperation> diff = state.buildDiff();
    355                 if (DEBUG) {
    356                     Log.v(TAG, "Content Provider Operations:");
    357                     for (ContentProviderOperation operation : diff) {
    358                         Log.v(TAG, operation.toString());
    359                     }
    360                 }
    361 
    362                 ContentProviderResult[] results = null;
    363                 if (!diff.isEmpty()) {
    364                     results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
    365                 }
    366 
    367                 final long rawContactId = getRawContactId(state, diff, results);
    368                 if (rawContactId == -1) {
    369                     throw new IllegalStateException("Could not determine RawContact ID after save");
    370                 }
    371                 // We don't have to check to see if the value is still -1.  If we reach here,
    372                 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
    373                 insertedRawContactId = getInsertedRawContactId(diff, results);
    374                 if (isProfile) {
    375                     // Since the profile supports local raw contacts, which may have been completely
    376                     // removed if all information was removed, we need to do a special query to
    377                     // get the lookup URI for the profile contact (if it still exists).
    378                     Cursor c = resolver.query(Profile.CONTENT_URI,
    379                             new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
    380                             null, null, null);
    381                     try {
    382                         if (c.moveToFirst()) {
    383                             final long contactId = c.getLong(0);
    384                             final String lookupKey = c.getString(1);
    385                             lookupUri = Contacts.getLookupUri(contactId, lookupKey);
    386                         }
    387                     } finally {
    388                         c.close();
    389                     }
    390                 } else {
    391                     final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
    392                                     rawContactId);
    393                     lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
    394                 }
    395                 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
    396 
    397                 // We can change this back to false later, if we fail to save the contact photo.
    398                 succeeded = true;
    399                 break;
    400 
    401             } catch (RemoteException e) {
    402                 // Something went wrong, bail without success
    403                 Log.e(TAG, "Problem persisting user edits", e);
    404                 break;
    405 
    406             } catch (IllegalArgumentException e) {
    407                 // This is thrown by applyBatch on malformed requests
    408                 Log.e(TAG, "Problem persisting user edits", e);
    409                 showToast(R.string.contactSavedErrorToast);
    410                 break;
    411 
    412             } catch (OperationApplicationException e) {
    413                 // Version consistency failed, re-parent change and try again
    414                 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
    415                 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
    416                 boolean first = true;
    417                 final int count = state.size();
    418                 for (int i = 0; i < count; i++) {
    419                     Long rawContactId = state.getRawContactId(i);
    420                     if (rawContactId != null && rawContactId != -1) {
    421                         if (!first) {
    422                             sb.append(',');
    423                         }
    424                         sb.append(rawContactId);
    425                         first = false;
    426                     }
    427                 }
    428                 sb.append(")");
    429 
    430                 if (first) {
    431                     throw new IllegalStateException(
    432                             "Version consistency failed for a new contact", e);
    433                 }
    434 
    435                 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
    436                         isProfile
    437                                 ? RawContactsEntity.PROFILE_CONTENT_URI
    438                                 : RawContactsEntity.CONTENT_URI,
    439                         resolver, sb.toString(), null, null);
    440                 state = RawContactDeltaList.mergeAfter(newState, state);
    441 
    442                 // Update the new state to use profile URIs if appropriate.
    443                 if (isProfile) {
    444                     for (RawContactDelta delta : state) {
    445                         delta.setProfileQueryUri();
    446                     }
    447                 }
    448             }
    449         }
    450 
    451         // Now save any updated photos.  We do this at the end to ensure that
    452         // the ContactProvider already knows about newly-created contacts.
    453         if (updatedPhotos != null) {
    454             for (String key : updatedPhotos.keySet()) {
    455                 Uri photoUri = updatedPhotos.getParcelable(key);
    456                 long rawContactId = Long.parseLong(key);
    457 
    458                 // If the raw-contact ID is negative, we are saving a new raw-contact;
    459                 // replace the bogus ID with the new one that we actually saved the contact at.
    460                 if (rawContactId < 0) {
    461                     rawContactId = insertedRawContactId;
    462                     if (rawContactId == -1) {
    463                         throw new IllegalStateException(
    464                                 "Could not determine RawContact ID for image insertion");
    465                     }
    466                 }
    467 
    468                 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
    469             }
    470         }
    471 
    472         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    473         if (callbackIntent != null) {
    474             if (succeeded) {
    475                 // Mark the intent to indicate that the save was successful (even if the lookup URI
    476                 // is now null).  For local contacts or the local profile, it's possible that the
    477                 // save triggered removal of the contact, so no lookup URI would exist..
    478                 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
    479             }
    480             callbackIntent.setData(lookupUri);
    481             deliverCallback(callbackIntent);
    482         }
    483     }
    484 
    485     /**
    486      * Save updated photo for the specified raw-contact.
    487      * @return true for success, false for failure
    488      */
    489     private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
    490         final Uri outputUri = Uri.withAppendedPath(
    491                 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
    492                 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
    493 
    494         return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
    495     }
    496 
    497     /**
    498      * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
    499      */
    500     private long getRawContactId(RawContactDeltaList state,
    501             final ArrayList<ContentProviderOperation> diff,
    502             final ContentProviderResult[] results) {
    503         long existingRawContactId = state.findRawContactId();
    504         if (existingRawContactId != -1) {
    505             return existingRawContactId;
    506         }
    507 
    508         return getInsertedRawContactId(diff, results);
    509     }
    510 
    511     /**
    512      * Find the ID of a newly-inserted raw-contact.  If none exists, return -1.
    513      */
    514     private long getInsertedRawContactId(
    515             final ArrayList<ContentProviderOperation> diff,
    516             final ContentProviderResult[] results) {
    517         final int diffSize = diff.size();
    518         for (int i = 0; i < diffSize; i++) {
    519             ContentProviderOperation operation = diff.get(i);
    520             if (operation.getType() == ContentProviderOperation.TYPE_INSERT
    521                     && operation.getUri().getEncodedPath().contains(
    522                             RawContacts.CONTENT_URI.getEncodedPath())) {
    523                 return ContentUris.parseId(results[i].uri);
    524             }
    525         }
    526         return -1;
    527     }
    528 
    529     /**
    530      * Creates an intent that can be sent to this service to create a new group as
    531      * well as add new members at the same time.
    532      *
    533      * @param context of the application
    534      * @param account in which the group should be created
    535      * @param label is the name of the group (cannot be null)
    536      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    537      *            should be added to the group
    538      * @param callbackActivity is the activity to send the callback intent to
    539      * @param callbackAction is the intent action for the callback intent
    540      */
    541     public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
    542             String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
    543             String callbackAction) {
    544         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    545         serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
    546         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
    547         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
    548         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
    549         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
    550         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    551 
    552         // Callback intent will be invoked by the service once the new group is
    553         // created.
    554         Intent callbackIntent = new Intent(context, callbackActivity);
    555         callbackIntent.setAction(callbackAction);
    556         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    557 
    558         return serviceIntent;
    559     }
    560 
    561     private void createGroup(Intent intent) {
    562         String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
    563         String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
    564         String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
    565         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    566         final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    567 
    568         ContentValues values = new ContentValues();
    569         values.put(Groups.ACCOUNT_TYPE, accountType);
    570         values.put(Groups.ACCOUNT_NAME, accountName);
    571         values.put(Groups.DATA_SET, dataSet);
    572         values.put(Groups.TITLE, label);
    573 
    574         final ContentResolver resolver = getContentResolver();
    575 
    576         // Create the new group
    577         final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
    578 
    579         // If there's no URI, then the insertion failed. Abort early because group members can't be
    580         // added if the group doesn't exist
    581         if (groupUri == null) {
    582             Log.e(TAG, "Couldn't create group with label " + label);
    583             return;
    584         }
    585 
    586         // Add new group members
    587         addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
    588 
    589         // TODO: Move this into the contact editor where it belongs. This needs to be integrated
    590         // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
    591         values.clear();
    592         values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    593         values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
    594 
    595         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    596         callbackIntent.setData(groupUri);
    597         // TODO: This can be taken out when the above TODO is addressed
    598         callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
    599         deliverCallback(callbackIntent);
    600     }
    601 
    602     /**
    603      * Creates an intent that can be sent to this service to rename a group.
    604      */
    605     public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
    606             Class<? extends Activity> callbackActivity, String callbackAction) {
    607         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    608         serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
    609         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    610         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    611 
    612         // Callback intent will be invoked by the service once the group is renamed.
    613         Intent callbackIntent = new Intent(context, callbackActivity);
    614         callbackIntent.setAction(callbackAction);
    615         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    616 
    617         return serviceIntent;
    618     }
    619 
    620     private void renameGroup(Intent intent) {
    621         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    622         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    623 
    624         if (groupId == -1) {
    625             Log.e(TAG, "Invalid arguments for renameGroup request");
    626             return;
    627         }
    628 
    629         ContentValues values = new ContentValues();
    630         values.put(Groups.TITLE, label);
    631         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    632         getContentResolver().update(groupUri, values, null, null);
    633 
    634         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    635         callbackIntent.setData(groupUri);
    636         deliverCallback(callbackIntent);
    637     }
    638 
    639     /**
    640      * Creates an intent that can be sent to this service to delete a group.
    641      */
    642     public static Intent createGroupDeletionIntent(Context context, long groupId) {
    643         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    644         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
    645         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    646         return serviceIntent;
    647     }
    648 
    649     private void deleteGroup(Intent intent) {
    650         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    651         if (groupId == -1) {
    652             Log.e(TAG, "Invalid arguments for deleteGroup request");
    653             return;
    654         }
    655 
    656         getContentResolver().delete(
    657                 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
    658     }
    659 
    660     /**
    661      * Creates an intent that can be sent to this service to rename a group as
    662      * well as add and remove members from the group.
    663      *
    664      * @param context of the application
    665      * @param groupId of the group that should be modified
    666      * @param newLabel is the updated name of the group (can be null if the name
    667      *            should not be updated)
    668      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    669      *            should be added to the group
    670      * @param rawContactsToRemove is an array of raw contact IDs for contacts
    671      *            that should be removed from the group
    672      * @param callbackActivity is the activity to send the callback intent to
    673      * @param callbackAction is the intent action for the callback intent
    674      */
    675     public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
    676             long[] rawContactsToAdd, long[] rawContactsToRemove,
    677             Class<? extends Activity> callbackActivity, String callbackAction) {
    678         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    679         serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
    680         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    681         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    682         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    683         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
    684                 rawContactsToRemove);
    685 
    686         // Callback intent will be invoked by the service once the group is updated
    687         Intent callbackIntent = new Intent(context, callbackActivity);
    688         callbackIntent.setAction(callbackAction);
    689         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    690 
    691         return serviceIntent;
    692     }
    693 
    694     private void updateGroup(Intent intent) {
    695         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    696         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    697         long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    698         long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
    699 
    700         if (groupId == -1) {
    701             Log.e(TAG, "Invalid arguments for updateGroup request");
    702             return;
    703         }
    704 
    705         final ContentResolver resolver = getContentResolver();
    706         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    707 
    708         // Update group name if necessary
    709         if (label != null) {
    710             ContentValues values = new ContentValues();
    711             values.put(Groups.TITLE, label);
    712             resolver.update(groupUri, values, null, null);
    713         }
    714 
    715         // Add and remove members if necessary
    716         addMembersToGroup(resolver, rawContactsToAdd, groupId);
    717         removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
    718 
    719         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    720         callbackIntent.setData(groupUri);
    721         deliverCallback(callbackIntent);
    722     }
    723 
    724     private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
    725             long groupId) {
    726         if (rawContactsToAdd == null) {
    727             return;
    728         }
    729         for (long rawContactId : rawContactsToAdd) {
    730             try {
    731                 final ArrayList<ContentProviderOperation> rawContactOperations =
    732                         new ArrayList<ContentProviderOperation>();
    733 
    734                 // Build an assert operation to ensure the contact is not already in the group
    735                 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
    736                         .newAssertQuery(Data.CONTENT_URI);
    737                 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
    738                         Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
    739                         new String[] { String.valueOf(rawContactId),
    740                         GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
    741                 assertBuilder.withExpectedCount(0);
    742                 rawContactOperations.add(assertBuilder.build());
    743 
    744                 // Build an insert operation to add the contact to the group
    745                 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
    746                         .newInsert(Data.CONTENT_URI);
    747                 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
    748                 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    749                 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
    750                 rawContactOperations.add(insertBuilder.build());
    751 
    752                 if (DEBUG) {
    753                     for (ContentProviderOperation operation : rawContactOperations) {
    754                         Log.v(TAG, operation.toString());
    755                     }
    756                 }
    757 
    758                 // Apply batch
    759                 if (!rawContactOperations.isEmpty()) {
    760                     resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
    761                 }
    762             } catch (RemoteException e) {
    763                 // Something went wrong, bail without success
    764                 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
    765                         String.valueOf(rawContactId), e);
    766             } catch (OperationApplicationException e) {
    767                 // The assert could have failed because the contact is already in the group,
    768                 // just continue to the next contact
    769                 Log.w(TAG, "Assert failed in adding raw contact ID " +
    770                         String.valueOf(rawContactId) + ". Already exists in group " +
    771                         String.valueOf(groupId), e);
    772             }
    773         }
    774     }
    775 
    776     private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
    777             long groupId) {
    778         if (rawContactsToRemove == null) {
    779             return;
    780         }
    781         for (long rawContactId : rawContactsToRemove) {
    782             // Apply the delete operation on the data row for the given raw contact's
    783             // membership in the given group. If no contact matches the provided selection, then
    784             // nothing will be done. Just continue to the next contact.
    785             resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
    786                     Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
    787                     new String[] { String.valueOf(rawContactId),
    788                     GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
    789         }
    790     }
    791 
    792     /**
    793      * Creates an intent that can be sent to this service to star or un-star a contact.
    794      */
    795     public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
    796         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    797         serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
    798         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    799         serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
    800 
    801         return serviceIntent;
    802     }
    803 
    804     private void setStarred(Intent intent) {
    805         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    806         boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
    807         if (contactUri == null) {
    808             Log.e(TAG, "Invalid arguments for setStarred request");
    809             return;
    810         }
    811 
    812         final ContentValues values = new ContentValues(1);
    813         values.put(Contacts.STARRED, value);
    814         getContentResolver().update(contactUri, values, null, null);
    815 
    816         // Undemote the contact if necessary
    817         final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
    818                 null, null, null);
    819         try {
    820             if (c.moveToFirst()) {
    821                 final long id = c.getLong(0);
    822 
    823                 // Don't bother undemoting if this contact is the user's profile.
    824                 if (id < Profile.MIN_ID) {
    825                     getContentResolver().call(ContactsContract.AUTHORITY_URI,
    826                             PinnedPositions.UNDEMOTE_METHOD, String.valueOf(id), null);
    827                 }
    828             }
    829         } finally {
    830             c.close();
    831         }
    832     }
    833 
    834     /**
    835      * Creates an intent that can be sent to this service to set the redirect to voicemail.
    836      */
    837     public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
    838             boolean value) {
    839         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    840         serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
    841         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    842         serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
    843 
    844         return serviceIntent;
    845     }
    846 
    847     private void setSendToVoicemail(Intent intent) {
    848         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    849         boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
    850         if (contactUri == null) {
    851             Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
    852             return;
    853         }
    854 
    855         final ContentValues values = new ContentValues(1);
    856         values.put(Contacts.SEND_TO_VOICEMAIL, value);
    857         getContentResolver().update(contactUri, values, null, null);
    858     }
    859 
    860     /**
    861      * Creates an intent that can be sent to this service to save the contact's ringtone.
    862      */
    863     public static Intent createSetRingtone(Context context, Uri contactUri,
    864             String value) {
    865         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    866         serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
    867         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    868         serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
    869 
    870         return serviceIntent;
    871     }
    872 
    873     private void setRingtone(Intent intent) {
    874         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    875         String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
    876         if (contactUri == null) {
    877             Log.e(TAG, "Invalid arguments for setRingtone");
    878             return;
    879         }
    880         ContentValues values = new ContentValues(1);
    881         values.put(Contacts.CUSTOM_RINGTONE, value);
    882         getContentResolver().update(contactUri, values, null, null);
    883     }
    884 
    885     /**
    886      * Creates an intent that sets the selected data item as super primary (default)
    887      */
    888     public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
    889         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    890         serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
    891         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
    892         return serviceIntent;
    893     }
    894 
    895     private void setSuperPrimary(Intent intent) {
    896         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
    897         if (dataId == -1) {
    898             Log.e(TAG, "Invalid arguments for setSuperPrimary request");
    899             return;
    900         }
    901 
    902         ContactUpdateUtils.setSuperPrimary(this, dataId);
    903     }
    904 
    905     /**
    906      * Creates an intent that clears the primary flag of all data items that belong to the same
    907      * raw_contact as the given data item. Will only clear, if the data item was primary before
    908      * this call
    909      */
    910     public static Intent createClearPrimaryIntent(Context context, long dataId) {
    911         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    912         serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
    913         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
    914         return serviceIntent;
    915     }
    916 
    917     private void clearPrimary(Intent intent) {
    918         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
    919         if (dataId == -1) {
    920             Log.e(TAG, "Invalid arguments for clearPrimary request");
    921             return;
    922         }
    923 
    924         // Update the primary values in the data record.
    925         ContentValues values = new ContentValues(1);
    926         values.put(Data.IS_SUPER_PRIMARY, 0);
    927         values.put(Data.IS_PRIMARY, 0);
    928 
    929         getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
    930                 values, null, null);
    931     }
    932 
    933     /**
    934      * Creates an intent that can be sent to this service to delete a contact.
    935      */
    936     public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
    937         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    938         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
    939         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    940         return serviceIntent;
    941     }
    942 
    943     private void deleteContact(Intent intent) {
    944         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    945         if (contactUri == null) {
    946             Log.e(TAG, "Invalid arguments for deleteContact request");
    947             return;
    948         }
    949 
    950         getContentResolver().delete(contactUri, null, null);
    951     }
    952 
    953     /**
    954      * Creates an intent that can be sent to this service to join two contacts.
    955      */
    956     public static Intent createJoinContactsIntent(Context context, long contactId1,
    957             long contactId2, boolean contactWritable,
    958             Class<? extends Activity> callbackActivity, String callbackAction) {
    959         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    960         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
    961         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
    962         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
    963         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
    964 
    965         // Callback intent will be invoked by the service once the contacts are joined.
    966         Intent callbackIntent = new Intent(context, callbackActivity);
    967         callbackIntent.setAction(callbackAction);
    968         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    969 
    970         return serviceIntent;
    971     }
    972 
    973 
    974     private interface JoinContactQuery {
    975         String[] PROJECTION = {
    976                 RawContacts._ID,
    977                 RawContacts.CONTACT_ID,
    978                 RawContacts.NAME_VERIFIED,
    979                 RawContacts.DISPLAY_NAME_SOURCE,
    980         };
    981 
    982         String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
    983 
    984         int _ID = 0;
    985         int CONTACT_ID = 1;
    986         int NAME_VERIFIED = 2;
    987         int DISPLAY_NAME_SOURCE = 3;
    988     }
    989 
    990     private void joinContacts(Intent intent) {
    991         long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
    992         long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
    993         boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
    994         if (contactId1 == -1 || contactId2 == -1) {
    995             Log.e(TAG, "Invalid arguments for joinContacts request");
    996             return;
    997         }
    998 
    999         final ContentResolver resolver = getContentResolver();
   1000 
   1001         // Load raw contact IDs for all raw contacts involved - currently edited and selected
   1002         // in the join UIs
   1003         Cursor c = resolver.query(RawContacts.CONTENT_URI,
   1004                 JoinContactQuery.PROJECTION,
   1005                 JoinContactQuery.SELECTION,
   1006                 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
   1007 
   1008         long rawContactIds[];
   1009         long verifiedNameRawContactId = -1;
   1010         try {
   1011             if (c.getCount() == 0) {
   1012                 return;
   1013             }
   1014             int maxDisplayNameSource = -1;
   1015             rawContactIds = new long[c.getCount()];
   1016             for (int i = 0; i < rawContactIds.length; i++) {
   1017                 c.moveToPosition(i);
   1018                 long rawContactId = c.getLong(JoinContactQuery._ID);
   1019                 rawContactIds[i] = rawContactId;
   1020                 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
   1021                 if (nameSource > maxDisplayNameSource) {
   1022                     maxDisplayNameSource = nameSource;
   1023                 }
   1024             }
   1025 
   1026             // Find an appropriate display name for the joined contact:
   1027             // if should have a higher DisplayNameSource or be the name
   1028             // of the original contact that we are joining with another.
   1029             if (writable) {
   1030                 for (int i = 0; i < rawContactIds.length; i++) {
   1031                     c.moveToPosition(i);
   1032                     if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
   1033                         int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
   1034                         if (nameSource == maxDisplayNameSource
   1035                                 && (verifiedNameRawContactId == -1
   1036                                         || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
   1037                             verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
   1038                         }
   1039                     }
   1040                 }
   1041             }
   1042         } finally {
   1043             c.close();
   1044         }
   1045 
   1046         // For each pair of raw contacts, insert an aggregation exception
   1047         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
   1048         for (int i = 0; i < rawContactIds.length; i++) {
   1049             for (int j = 0; j < rawContactIds.length; j++) {
   1050                 if (i != j) {
   1051                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
   1052                 }
   1053             }
   1054         }
   1055 
   1056         // Mark the original contact as "name verified" to make sure that the contact
   1057         // display name does not change as a result of the join
   1058         if (verifiedNameRawContactId != -1) {
   1059             Builder builder = ContentProviderOperation.newUpdate(
   1060                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
   1061             builder.withValue(RawContacts.NAME_VERIFIED, 1);
   1062             operations.add(builder.build());
   1063         }
   1064 
   1065         boolean success = false;
   1066         // Apply all aggregation exceptions as one batch
   1067         try {
   1068             resolver.applyBatch(ContactsContract.AUTHORITY, operations);
   1069             showToast(R.string.contactsJoinedMessage);
   1070             success = true;
   1071         } catch (RemoteException e) {
   1072             Log.e(TAG, "Failed to apply aggregation exception batch", e);
   1073             showToast(R.string.contactSavedErrorToast);
   1074         } catch (OperationApplicationException e) {
   1075             Log.e(TAG, "Failed to apply aggregation exception batch", e);
   1076             showToast(R.string.contactSavedErrorToast);
   1077         }
   1078 
   1079         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
   1080         if (success) {
   1081             Uri uri = RawContacts.getContactLookupUri(resolver,
   1082                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
   1083             callbackIntent.setData(uri);
   1084         }
   1085         deliverCallback(callbackIntent);
   1086     }
   1087 
   1088     /**
   1089      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
   1090      */
   1091     private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
   1092             long rawContactId1, long rawContactId2) {
   1093         Builder builder =
   1094                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
   1095         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
   1096         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   1097         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   1098         operations.add(builder.build());
   1099     }
   1100 
   1101     /**
   1102      * Shows a toast on the UI thread.
   1103      */
   1104     private void showToast(final int message) {
   1105         mMainHandler.post(new Runnable() {
   1106 
   1107             @Override
   1108             public void run() {
   1109                 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
   1110             }
   1111         });
   1112     }
   1113 
   1114     private void deliverCallback(final Intent callbackIntent) {
   1115         mMainHandler.post(new Runnable() {
   1116 
   1117             @Override
   1118             public void run() {
   1119                 deliverCallbackOnUiThread(callbackIntent);
   1120             }
   1121         });
   1122     }
   1123 
   1124     void deliverCallbackOnUiThread(final Intent callbackIntent) {
   1125         // TODO: this assumes that if there are multiple instances of the same
   1126         // activity registered, the last one registered is the one waiting for
   1127         // the callback. Validity of this assumption needs to be verified.
   1128         for (Listener listener : sListeners) {
   1129             if (callbackIntent.getComponent().equals(
   1130                     ((Activity) listener).getIntent().getComponent())) {
   1131                 listener.onServiceCompleted(callbackIntent);
   1132                 return;
   1133             }
   1134         }
   1135     }
   1136 }
   1137