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 (IllegalArgumentException e) {
    414                 // This is thrown by applyBatch on malformed requests
    415                 Log.e(TAG, "Problem persisting user edits", e);
    416                 showToast(R.string.contactSavedErrorToast);
    417                 break;
    418 
    419             } catch (OperationApplicationException e) {
    420                 // Version consistency failed, re-parent change and try again
    421                 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
    422                 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
    423                 boolean first = true;
    424                 final int count = state.size();
    425                 for (int i = 0; i < count; i++) {
    426                     Long rawContactId = state.getRawContactId(i);
    427                     if (rawContactId != null && rawContactId != -1) {
    428                         if (!first) {
    429                             sb.append(',');
    430                         }
    431                         sb.append(rawContactId);
    432                         first = false;
    433                     }
    434                 }
    435                 sb.append(")");
    436 
    437                 if (first) {
    438                     throw new IllegalStateException(
    439                             "Version consistency failed for a new contact", e);
    440                 }
    441 
    442                 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
    443                         isProfile
    444                                 ? RawContactsEntity.PROFILE_CONTENT_URI
    445                                 : RawContactsEntity.CONTENT_URI,
    446                         resolver, sb.toString(), null, null);
    447                 state = RawContactDeltaList.mergeAfter(newState, state);
    448 
    449                 // Update the new state to use profile URIs if appropriate.
    450                 if (isProfile) {
    451                     for (RawContactDelta delta : state) {
    452                         delta.setProfileQueryUri();
    453                     }
    454                 }
    455             }
    456         }
    457 
    458         // Now save any updated photos.  We do this at the end to ensure that
    459         // the ContactProvider already knows about newly-created contacts.
    460         if (updatedPhotos != null) {
    461             for (String key : updatedPhotos.keySet()) {
    462                 Uri photoUri = updatedPhotos.getParcelable(key);
    463                 long rawContactId = Long.parseLong(key);
    464 
    465                 // If the raw-contact ID is negative, we are saving a new raw-contact;
    466                 // replace the bogus ID with the new one that we actually saved the contact at.
    467                 if (rawContactId < 0) {
    468                     rawContactId = insertedRawContactId;
    469                     if (rawContactId == -1) {
    470                         throw new IllegalStateException(
    471                                 "Could not determine RawContact ID for image insertion");
    472                     }
    473                 }
    474 
    475                 if (!saveUpdatedPhoto(rawContactId, photoUri)) succeeded = false;
    476             }
    477         }
    478 
    479         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    480         if (callbackIntent != null) {
    481             if (succeeded) {
    482                 // Mark the intent to indicate that the save was successful (even if the lookup URI
    483                 // is now null).  For local contacts or the local profile, it's possible that the
    484                 // save triggered removal of the contact, so no lookup URI would exist..
    485                 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
    486             }
    487             callbackIntent.setData(lookupUri);
    488             deliverCallback(callbackIntent);
    489         }
    490     }
    491 
    492     /**
    493      * Save updated photo for the specified raw-contact.
    494      * @return true for success, false for failure
    495      */
    496     private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri) {
    497         final Uri outputUri = Uri.withAppendedPath(
    498                 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
    499                 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
    500 
    501         return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, true);
    502     }
    503 
    504     /**
    505      * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
    506      */
    507     private long getRawContactId(RawContactDeltaList state,
    508             final ArrayList<ContentProviderOperation> diff,
    509             final ContentProviderResult[] results) {
    510         long existingRawContactId = state.findRawContactId();
    511         if (existingRawContactId != -1) {
    512             return existingRawContactId;
    513         }
    514 
    515         return getInsertedRawContactId(diff, results);
    516     }
    517 
    518     /**
    519      * Find the ID of a newly-inserted raw-contact.  If none exists, return -1.
    520      */
    521     private long getInsertedRawContactId(
    522             final ArrayList<ContentProviderOperation> diff,
    523             final ContentProviderResult[] results) {
    524         final int diffSize = diff.size();
    525         for (int i = 0; i < diffSize; i++) {
    526             ContentProviderOperation operation = diff.get(i);
    527             if (operation.getType() == ContentProviderOperation.TYPE_INSERT
    528                     && operation.getUri().getEncodedPath().contains(
    529                             RawContacts.CONTENT_URI.getEncodedPath())) {
    530                 return ContentUris.parseId(results[i].uri);
    531             }
    532         }
    533         return -1;
    534     }
    535 
    536     /**
    537      * Creates an intent that can be sent to this service to create a new group as
    538      * well as add new members at the same time.
    539      *
    540      * @param context of the application
    541      * @param account in which the group should be created
    542      * @param label is the name of the group (cannot be null)
    543      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    544      *            should be added to the group
    545      * @param callbackActivity is the activity to send the callback intent to
    546      * @param callbackAction is the intent action for the callback intent
    547      */
    548     public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
    549             String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
    550             String callbackAction) {
    551         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    552         serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
    553         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
    554         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
    555         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
    556         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
    557         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    558 
    559         // Callback intent will be invoked by the service once the new group is
    560         // created.
    561         Intent callbackIntent = new Intent(context, callbackActivity);
    562         callbackIntent.setAction(callbackAction);
    563         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    564 
    565         return serviceIntent;
    566     }
    567 
    568     private void createGroup(Intent intent) {
    569         String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
    570         String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
    571         String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
    572         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    573         final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    574 
    575         ContentValues values = new ContentValues();
    576         values.put(Groups.ACCOUNT_TYPE, accountType);
    577         values.put(Groups.ACCOUNT_NAME, accountName);
    578         values.put(Groups.DATA_SET, dataSet);
    579         values.put(Groups.TITLE, label);
    580 
    581         final ContentResolver resolver = getContentResolver();
    582 
    583         // Create the new group
    584         final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
    585 
    586         // If there's no URI, then the insertion failed. Abort early because group members can't be
    587         // added if the group doesn't exist
    588         if (groupUri == null) {
    589             Log.e(TAG, "Couldn't create group with label " + label);
    590             return;
    591         }
    592 
    593         // Add new group members
    594         addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
    595 
    596         // TODO: Move this into the contact editor where it belongs. This needs to be integrated
    597         // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
    598         values.clear();
    599         values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    600         values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
    601 
    602         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    603         callbackIntent.setData(groupUri);
    604         // TODO: This can be taken out when the above TODO is addressed
    605         callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
    606         deliverCallback(callbackIntent);
    607     }
    608 
    609     /**
    610      * Creates an intent that can be sent to this service to rename a group.
    611      */
    612     public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
    613             Class<? extends Activity> callbackActivity, String callbackAction) {
    614         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    615         serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
    616         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    617         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    618 
    619         // Callback intent will be invoked by the service once the group is renamed.
    620         Intent callbackIntent = new Intent(context, callbackActivity);
    621         callbackIntent.setAction(callbackAction);
    622         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    623 
    624         return serviceIntent;
    625     }
    626 
    627     private void renameGroup(Intent intent) {
    628         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    629         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    630 
    631         if (groupId == -1) {
    632             Log.e(TAG, "Invalid arguments for renameGroup request");
    633             return;
    634         }
    635 
    636         ContentValues values = new ContentValues();
    637         values.put(Groups.TITLE, label);
    638         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    639         getContentResolver().update(groupUri, values, null, null);
    640 
    641         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    642         callbackIntent.setData(groupUri);
    643         deliverCallback(callbackIntent);
    644     }
    645 
    646     /**
    647      * Creates an intent that can be sent to this service to delete a group.
    648      */
    649     public static Intent createGroupDeletionIntent(Context context, long groupId) {
    650         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    651         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
    652         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    653         return serviceIntent;
    654     }
    655 
    656     private void deleteGroup(Intent intent) {
    657         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    658         if (groupId == -1) {
    659             Log.e(TAG, "Invalid arguments for deleteGroup request");
    660             return;
    661         }
    662 
    663         getContentResolver().delete(
    664                 ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
    665     }
    666 
    667     /**
    668      * Creates an intent that can be sent to this service to rename a group as
    669      * well as add and remove members from the group.
    670      *
    671      * @param context of the application
    672      * @param groupId of the group that should be modified
    673      * @param newLabel is the updated name of the group (can be null if the name
    674      *            should not be updated)
    675      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    676      *            should be added to the group
    677      * @param rawContactsToRemove is an array of raw contact IDs for contacts
    678      *            that should be removed from the group
    679      * @param callbackActivity is the activity to send the callback intent to
    680      * @param callbackAction is the intent action for the callback intent
    681      */
    682     public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
    683             long[] rawContactsToAdd, long[] rawContactsToRemove,
    684             Class<? extends Activity> callbackActivity, String callbackAction) {
    685         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    686         serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
    687         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    688         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    689         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    690         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
    691                 rawContactsToRemove);
    692 
    693         // Callback intent will be invoked by the service once the group is updated
    694         Intent callbackIntent = new Intent(context, callbackActivity);
    695         callbackIntent.setAction(callbackAction);
    696         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    697 
    698         return serviceIntent;
    699     }
    700 
    701     private void updateGroup(Intent intent) {
    702         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    703         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    704         long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    705         long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
    706 
    707         if (groupId == -1) {
    708             Log.e(TAG, "Invalid arguments for updateGroup request");
    709             return;
    710         }
    711 
    712         final ContentResolver resolver = getContentResolver();
    713         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    714 
    715         // Update group name if necessary
    716         if (label != null) {
    717             ContentValues values = new ContentValues();
    718             values.put(Groups.TITLE, label);
    719             resolver.update(groupUri, values, null, null);
    720         }
    721 
    722         // Add and remove members if necessary
    723         addMembersToGroup(resolver, rawContactsToAdd, groupId);
    724         removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
    725 
    726         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    727         callbackIntent.setData(groupUri);
    728         deliverCallback(callbackIntent);
    729     }
    730 
    731     private static void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
    732             long groupId) {
    733         if (rawContactsToAdd == null) {
    734             return;
    735         }
    736         for (long rawContactId : rawContactsToAdd) {
    737             try {
    738                 final ArrayList<ContentProviderOperation> rawContactOperations =
    739                         new ArrayList<ContentProviderOperation>();
    740 
    741                 // Build an assert operation to ensure the contact is not already in the group
    742                 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
    743                         .newAssertQuery(Data.CONTENT_URI);
    744                 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
    745                         Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
    746                         new String[] { String.valueOf(rawContactId),
    747                         GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
    748                 assertBuilder.withExpectedCount(0);
    749                 rawContactOperations.add(assertBuilder.build());
    750 
    751                 // Build an insert operation to add the contact to the group
    752                 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
    753                         .newInsert(Data.CONTENT_URI);
    754                 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
    755                 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    756                 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
    757                 rawContactOperations.add(insertBuilder.build());
    758 
    759                 if (DEBUG) {
    760                     for (ContentProviderOperation operation : rawContactOperations) {
    761                         Log.v(TAG, operation.toString());
    762                     }
    763                 }
    764 
    765                 // Apply batch
    766                 if (!rawContactOperations.isEmpty()) {
    767                     resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
    768                 }
    769             } catch (RemoteException e) {
    770                 // Something went wrong, bail without success
    771                 Log.e(TAG, "Problem persisting user edits for raw contact ID " +
    772                         String.valueOf(rawContactId), e);
    773             } catch (OperationApplicationException e) {
    774                 // The assert could have failed because the contact is already in the group,
    775                 // just continue to the next contact
    776                 Log.w(TAG, "Assert failed in adding raw contact ID " +
    777                         String.valueOf(rawContactId) + ". Already exists in group " +
    778                         String.valueOf(groupId), e);
    779             }
    780         }
    781     }
    782 
    783     private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
    784             long groupId) {
    785         if (rawContactsToRemove == null) {
    786             return;
    787         }
    788         for (long rawContactId : rawContactsToRemove) {
    789             // Apply the delete operation on the data row for the given raw contact's
    790             // membership in the given group. If no contact matches the provided selection, then
    791             // nothing will be done. Just continue to the next contact.
    792             resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
    793                     Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
    794                     new String[] { String.valueOf(rawContactId),
    795                     GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
    796         }
    797     }
    798 
    799     /**
    800      * Creates an intent that can be sent to this service to star or un-star a contact.
    801      */
    802     public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
    803         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    804         serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
    805         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    806         serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
    807 
    808         return serviceIntent;
    809     }
    810 
    811     private void setStarred(Intent intent) {
    812         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    813         boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
    814         if (contactUri == null) {
    815             Log.e(TAG, "Invalid arguments for setStarred request");
    816             return;
    817         }
    818 
    819         final ContentValues values = new ContentValues(1);
    820         values.put(Contacts.STARRED, value);
    821         getContentResolver().update(contactUri, values, null, null);
    822 
    823         // Undemote the contact if necessary
    824         final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
    825                 null, null, null);
    826         try {
    827             if (c.moveToFirst()) {
    828                 final long id = c.getLong(0);
    829 
    830                 // Don't bother undemoting if this contact is the user's profile.
    831                 if (id < Profile.MIN_ID) {
    832                     values.clear();
    833                     values.put(String.valueOf(id), PinnedPositions.UNDEMOTE);
    834                     getContentResolver().update(PinnedPositions.UPDATE_URI, values, null, null);
    835                 }
    836             }
    837         } finally {
    838             c.close();
    839         }
    840     }
    841 
    842     /**
    843      * Creates an intent that can be sent to this service to set the redirect to voicemail.
    844      */
    845     public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
    846             boolean value) {
    847         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    848         serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
    849         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    850         serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
    851 
    852         return serviceIntent;
    853     }
    854 
    855     private void setSendToVoicemail(Intent intent) {
    856         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    857         boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
    858         if (contactUri == null) {
    859             Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
    860             return;
    861         }
    862 
    863         final ContentValues values = new ContentValues(1);
    864         values.put(Contacts.SEND_TO_VOICEMAIL, value);
    865         getContentResolver().update(contactUri, values, null, null);
    866     }
    867 
    868     /**
    869      * Creates an intent that can be sent to this service to save the contact's ringtone.
    870      */
    871     public static Intent createSetRingtone(Context context, Uri contactUri,
    872             String value) {
    873         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    874         serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
    875         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    876         serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
    877 
    878         return serviceIntent;
    879     }
    880 
    881     private void setRingtone(Intent intent) {
    882         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    883         String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
    884         if (contactUri == null) {
    885             Log.e(TAG, "Invalid arguments for setRingtone");
    886             return;
    887         }
    888         ContentValues values = new ContentValues(1);
    889         values.put(Contacts.CUSTOM_RINGTONE, value);
    890         getContentResolver().update(contactUri, values, null, null);
    891     }
    892 
    893     /**
    894      * Creates an intent that sets the selected data item as super primary (default)
    895      */
    896     public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
    897         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    898         serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
    899         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
    900         return serviceIntent;
    901     }
    902 
    903     private void setSuperPrimary(Intent intent) {
    904         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
    905         if (dataId == -1) {
    906             Log.e(TAG, "Invalid arguments for setSuperPrimary request");
    907             return;
    908         }
    909 
    910         ContactUpdateUtils.setSuperPrimary(this, dataId);
    911     }
    912 
    913     /**
    914      * Creates an intent that clears the primary flag of all data items that belong to the same
    915      * raw_contact as the given data item. Will only clear, if the data item was primary before
    916      * this call
    917      */
    918     public static Intent createClearPrimaryIntent(Context context, long dataId) {
    919         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    920         serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
    921         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
    922         return serviceIntent;
    923     }
    924 
    925     private void clearPrimary(Intent intent) {
    926         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
    927         if (dataId == -1) {
    928             Log.e(TAG, "Invalid arguments for clearPrimary request");
    929             return;
    930         }
    931 
    932         // Update the primary values in the data record.
    933         ContentValues values = new ContentValues(1);
    934         values.put(Data.IS_SUPER_PRIMARY, 0);
    935         values.put(Data.IS_PRIMARY, 0);
    936 
    937         getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
    938                 values, null, null);
    939     }
    940 
    941     /**
    942      * Creates an intent that can be sent to this service to delete a contact.
    943      */
    944     public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
    945         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    946         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
    947         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
    948         return serviceIntent;
    949     }
    950 
    951     private void deleteContact(Intent intent) {
    952         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
    953         if (contactUri == null) {
    954             Log.e(TAG, "Invalid arguments for deleteContact request");
    955             return;
    956         }
    957 
    958         getContentResolver().delete(contactUri, null, null);
    959     }
    960 
    961     /**
    962      * Creates an intent that can be sent to this service to join two contacts.
    963      */
    964     public static Intent createJoinContactsIntent(Context context, long contactId1,
    965             long contactId2, boolean contactWritable,
    966             Class<? extends Activity> callbackActivity, String callbackAction) {
    967         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    968         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
    969         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
    970         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
    971         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_WRITABLE, contactWritable);
    972 
    973         // Callback intent will be invoked by the service once the contacts are joined.
    974         Intent callbackIntent = new Intent(context, callbackActivity);
    975         callbackIntent.setAction(callbackAction);
    976         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    977 
    978         return serviceIntent;
    979     }
    980 
    981 
    982     private interface JoinContactQuery {
    983         String[] PROJECTION = {
    984                 RawContacts._ID,
    985                 RawContacts.CONTACT_ID,
    986                 RawContacts.NAME_VERIFIED,
    987                 RawContacts.DISPLAY_NAME_SOURCE,
    988         };
    989 
    990         String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
    991 
    992         int _ID = 0;
    993         int CONTACT_ID = 1;
    994         int NAME_VERIFIED = 2;
    995         int DISPLAY_NAME_SOURCE = 3;
    996     }
    997 
    998     private void joinContacts(Intent intent) {
    999         long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
   1000         long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
   1001         boolean writable = intent.getBooleanExtra(EXTRA_CONTACT_WRITABLE, false);
   1002         if (contactId1 == -1 || contactId2 == -1) {
   1003             Log.e(TAG, "Invalid arguments for joinContacts request");
   1004             return;
   1005         }
   1006 
   1007         final ContentResolver resolver = getContentResolver();
   1008 
   1009         // Load raw contact IDs for all raw contacts involved - currently edited and selected
   1010         // in the join UIs
   1011         Cursor c = resolver.query(RawContacts.CONTENT_URI,
   1012                 JoinContactQuery.PROJECTION,
   1013                 JoinContactQuery.SELECTION,
   1014                 new String[]{String.valueOf(contactId1), String.valueOf(contactId2)}, null);
   1015 
   1016         long rawContactIds[];
   1017         long verifiedNameRawContactId = -1;
   1018         try {
   1019             if (c.getCount() == 0) {
   1020                 return;
   1021             }
   1022             int maxDisplayNameSource = -1;
   1023             rawContactIds = new long[c.getCount()];
   1024             for (int i = 0; i < rawContactIds.length; i++) {
   1025                 c.moveToPosition(i);
   1026                 long rawContactId = c.getLong(JoinContactQuery._ID);
   1027                 rawContactIds[i] = rawContactId;
   1028                 int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
   1029                 if (nameSource > maxDisplayNameSource) {
   1030                     maxDisplayNameSource = nameSource;
   1031                 }
   1032             }
   1033 
   1034             // Find an appropriate display name for the joined contact:
   1035             // if should have a higher DisplayNameSource or be the name
   1036             // of the original contact that we are joining with another.
   1037             if (writable) {
   1038                 for (int i = 0; i < rawContactIds.length; i++) {
   1039                     c.moveToPosition(i);
   1040                     if (c.getLong(JoinContactQuery.CONTACT_ID) == contactId1) {
   1041                         int nameSource = c.getInt(JoinContactQuery.DISPLAY_NAME_SOURCE);
   1042                         if (nameSource == maxDisplayNameSource
   1043                                 && (verifiedNameRawContactId == -1
   1044                                         || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0)) {
   1045                             verifiedNameRawContactId = c.getLong(JoinContactQuery._ID);
   1046                         }
   1047                     }
   1048                 }
   1049             }
   1050         } finally {
   1051             c.close();
   1052         }
   1053 
   1054         // For each pair of raw contacts, insert an aggregation exception
   1055         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
   1056         for (int i = 0; i < rawContactIds.length; i++) {
   1057             for (int j = 0; j < rawContactIds.length; j++) {
   1058                 if (i != j) {
   1059                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
   1060                 }
   1061             }
   1062         }
   1063 
   1064         // Mark the original contact as "name verified" to make sure that the contact
   1065         // display name does not change as a result of the join
   1066         if (verifiedNameRawContactId != -1) {
   1067             Builder builder = ContentProviderOperation.newUpdate(
   1068                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
   1069             builder.withValue(RawContacts.NAME_VERIFIED, 1);
   1070             operations.add(builder.build());
   1071         }
   1072 
   1073         boolean success = false;
   1074         // Apply all aggregation exceptions as one batch
   1075         try {
   1076             resolver.applyBatch(ContactsContract.AUTHORITY, operations);
   1077             showToast(R.string.contactsJoinedMessage);
   1078             success = true;
   1079         } catch (RemoteException e) {
   1080             Log.e(TAG, "Failed to apply aggregation exception batch", e);
   1081             showToast(R.string.contactSavedErrorToast);
   1082         } catch (OperationApplicationException e) {
   1083             Log.e(TAG, "Failed to apply aggregation exception batch", e);
   1084             showToast(R.string.contactSavedErrorToast);
   1085         }
   1086 
   1087         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
   1088         if (success) {
   1089             Uri uri = RawContacts.getContactLookupUri(resolver,
   1090                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
   1091             callbackIntent.setData(uri);
   1092         }
   1093         deliverCallback(callbackIntent);
   1094     }
   1095 
   1096     /**
   1097      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
   1098      */
   1099     private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
   1100             long rawContactId1, long rawContactId2) {
   1101         Builder builder =
   1102                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
   1103         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
   1104         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   1105         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   1106         operations.add(builder.build());
   1107     }
   1108 
   1109     /**
   1110      * Shows a toast on the UI thread.
   1111      */
   1112     private void showToast(final int message) {
   1113         mMainHandler.post(new Runnable() {
   1114 
   1115             @Override
   1116             public void run() {
   1117                 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
   1118             }
   1119         });
   1120     }
   1121 
   1122     private void deliverCallback(final Intent callbackIntent) {
   1123         mMainHandler.post(new Runnable() {
   1124 
   1125             @Override
   1126             public void run() {
   1127                 deliverCallbackOnUiThread(callbackIntent);
   1128             }
   1129         });
   1130     }
   1131 
   1132     void deliverCallbackOnUiThread(final Intent callbackIntent) {
   1133         // TODO: this assumes that if there are multiple instances of the same
   1134         // activity registered, the last one registered is the one waiting for
   1135         // the callback. Validity of this assumption needs to be verified.
   1136         for (Listener listener : sListeners) {
   1137             if (callbackIntent.getComponent().equals(
   1138                     ((Activity) listener).getIntent().getComponent())) {
   1139                 listener.onServiceCompleted(callbackIntent);
   1140                 return;
   1141             }
   1142         }
   1143     }
   1144 }
   1145