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