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