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