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