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 static android.Manifest.permission.WRITE_CONTACTS;
     20 
     21 import android.app.Activity;
     22 import android.app.IntentService;
     23 import android.content.ContentProviderOperation;
     24 import android.content.ContentProviderOperation.Builder;
     25 import android.content.ContentProviderResult;
     26 import android.content.ContentResolver;
     27 import android.content.ContentUris;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.OperationApplicationException;
     32 import android.database.Cursor;
     33 import android.database.DatabaseUtils;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.Handler;
     37 import android.os.Looper;
     38 import android.os.Parcelable;
     39 import android.os.RemoteException;
     40 import android.provider.ContactsContract;
     41 import android.provider.ContactsContract.AggregationExceptions;
     42 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     43 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     44 import android.provider.ContactsContract.Contacts;
     45 import android.provider.ContactsContract.Data;
     46 import android.provider.ContactsContract.Groups;
     47 import android.provider.ContactsContract.Profile;
     48 import android.provider.ContactsContract.RawContacts;
     49 import android.provider.ContactsContract.RawContactsEntity;
     50 import android.support.v4.content.LocalBroadcastManager;
     51 import android.support.v4.os.ResultReceiver;
     52 import android.text.TextUtils;
     53 import android.util.Log;
     54 import android.widget.Toast;
     55 
     56 import com.android.contacts.activities.ContactEditorActivity;
     57 import com.android.contacts.compat.CompatUtils;
     58 import com.android.contacts.compat.PinnedPositionsCompat;
     59 import com.android.contacts.database.ContactUpdateUtils;
     60 import com.android.contacts.database.SimContactDao;
     61 import com.android.contacts.model.AccountTypeManager;
     62 import com.android.contacts.model.CPOWrapper;
     63 import com.android.contacts.model.RawContactDelta;
     64 import com.android.contacts.model.RawContactDeltaList;
     65 import com.android.contacts.model.RawContactModifier;
     66 import com.android.contacts.model.account.AccountWithDataSet;
     67 import com.android.contacts.preference.ContactsPreferences;
     68 import com.android.contacts.util.ContactDisplayUtils;
     69 import com.android.contacts.util.ContactPhotoUtils;
     70 import com.android.contacts.util.PermissionsUtil;
     71 import com.android.contactsbind.FeedbackHelper;
     72 
     73 import com.google.common.collect.Lists;
     74 import com.google.common.collect.Sets;
     75 
     76 import java.util.ArrayList;
     77 import java.util.Collection;
     78 import java.util.HashSet;
     79 import java.util.List;
     80 import java.util.concurrent.CopyOnWriteArrayList;
     81 
     82 /**
     83  * A service responsible for saving changes to the content provider.
     84  */
     85 public class ContactSaveService extends IntentService {
     86     private static final String TAG = "ContactSaveService";
     87 
     88     /** Set to true in order to view logs on content provider operations */
     89     private static final boolean DEBUG = false;
     90 
     91     public static final String ACTION_NEW_RAW_CONTACT = "newRawContact";
     92 
     93     public static final String EXTRA_ACCOUNT_NAME = "accountName";
     94     public static final String EXTRA_ACCOUNT_TYPE = "accountType";
     95     public static final String EXTRA_DATA_SET = "dataSet";
     96     public static final String EXTRA_ACCOUNT = "account";
     97     public static final String EXTRA_CONTENT_VALUES = "contentValues";
     98     public static final String EXTRA_CALLBACK_INTENT = "callbackIntent";
     99     public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
    100     public static final String EXTRA_RAW_CONTACT_IDS = "rawContactIds";
    101 
    102     public static final String ACTION_SAVE_CONTACT = "saveContact";
    103     public static final String EXTRA_CONTACT_STATE = "state";
    104     public static final String EXTRA_SAVE_MODE = "saveMode";
    105     public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
    106     public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
    107     public static final String EXTRA_UPDATED_PHOTOS = "updatedPhotos";
    108 
    109     public static final String ACTION_CREATE_GROUP = "createGroup";
    110     public static final String ACTION_RENAME_GROUP = "renameGroup";
    111     public static final String ACTION_DELETE_GROUP = "deleteGroup";
    112     public static final String ACTION_UPDATE_GROUP = "updateGroup";
    113     public static final String EXTRA_GROUP_ID = "groupId";
    114     public static final String EXTRA_GROUP_LABEL = "groupLabel";
    115     public static final String EXTRA_RAW_CONTACTS_TO_ADD = "rawContactsToAdd";
    116     public static final String EXTRA_RAW_CONTACTS_TO_REMOVE = "rawContactsToRemove";
    117 
    118     public static final String ACTION_SET_STARRED = "setStarred";
    119     public static final String ACTION_DELETE_CONTACT = "delete";
    120     public static final String ACTION_DELETE_MULTIPLE_CONTACTS = "deleteMultipleContacts";
    121     public static final String EXTRA_CONTACT_URI = "contactUri";
    122     public static final String EXTRA_CONTACT_IDS = "contactIds";
    123     public static final String EXTRA_STARRED_FLAG = "starred";
    124     public static final String EXTRA_DISPLAY_NAME = "extraDisplayName";
    125     public static final String EXTRA_DISPLAY_NAME_ARRAY = "extraDisplayNameArray";
    126 
    127     public static final String ACTION_SET_SUPER_PRIMARY = "setSuperPrimary";
    128     public static final String ACTION_CLEAR_PRIMARY = "clearPrimary";
    129     public static final String EXTRA_DATA_ID = "dataId";
    130 
    131     public static final String ACTION_SPLIT_CONTACT = "splitContact";
    132     public static final String EXTRA_HARD_SPLIT = "extraHardSplit";
    133 
    134     public static final String ACTION_JOIN_CONTACTS = "joinContacts";
    135     public static final String ACTION_JOIN_SEVERAL_CONTACTS = "joinSeveralContacts";
    136     public static final String EXTRA_CONTACT_ID1 = "contactId1";
    137     public static final String EXTRA_CONTACT_ID2 = "contactId2";
    138 
    139     public static final String ACTION_SET_SEND_TO_VOICEMAIL = "sendToVoicemail";
    140     public static final String EXTRA_SEND_TO_VOICEMAIL_FLAG = "sendToVoicemailFlag";
    141 
    142     public static final String ACTION_SET_RINGTONE = "setRingtone";
    143     public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";
    144 
    145     public static final String ACTION_UNDO = "undo";
    146     public static final String EXTRA_UNDO_ACTION = "undoAction";
    147     public static final String EXTRA_UNDO_DATA = "undoData";
    148 
    149     // For debugging and testing what happens when requests are queued up.
    150     public static final String ACTION_SLEEP = "sleep";
    151     public static final String EXTRA_SLEEP_DURATION = "sleepDuration";
    152 
    153     public static final String BROADCAST_GROUP_DELETED = "groupDeleted";
    154     public static final String BROADCAST_LINK_COMPLETE = "linkComplete";
    155     public static final String BROADCAST_UNLINK_COMPLETE = "unlinkComplete";
    156 
    157     public static final String BROADCAST_SERVICE_STATE_CHANGED = "serviceStateChanged";
    158 
    159     public static final String EXTRA_RESULT_CODE = "resultCode";
    160     public static final String EXTRA_RESULT_COUNT = "count";
    161 
    162     public static final int CP2_ERROR = 0;
    163     public static final int CONTACTS_LINKED = 1;
    164     public static final int CONTACTS_SPLIT = 2;
    165     public static final int BAD_ARGUMENTS = 3;
    166     public static final int RESULT_UNKNOWN = 0;
    167     public static final int RESULT_SUCCESS = 1;
    168     public static final int RESULT_FAILURE = 2;
    169 
    170     private static final HashSet<String> ALLOWED_DATA_COLUMNS = Sets.newHashSet(
    171         Data.MIMETYPE,
    172         Data.IS_PRIMARY,
    173         Data.DATA1,
    174         Data.DATA2,
    175         Data.DATA3,
    176         Data.DATA4,
    177         Data.DATA5,
    178         Data.DATA6,
    179         Data.DATA7,
    180         Data.DATA8,
    181         Data.DATA9,
    182         Data.DATA10,
    183         Data.DATA11,
    184         Data.DATA12,
    185         Data.DATA13,
    186         Data.DATA14,
    187         Data.DATA15
    188     );
    189 
    190     private static final int PERSIST_TRIES = 3;
    191 
    192     private static final int MAX_CONTACTS_PROVIDER_BATCH_SIZE = 499;
    193 
    194     public interface Listener {
    195         public void onServiceCompleted(Intent callbackIntent);
    196     }
    197 
    198     private static final CopyOnWriteArrayList<Listener> sListeners =
    199             new CopyOnWriteArrayList<Listener>();
    200 
    201     // Holds the current state of the service
    202     private static final State sState = new State();
    203 
    204     private Handler mMainHandler;
    205     private GroupsDao mGroupsDao;
    206     private SimContactDao mSimContactDao;
    207 
    208     public ContactSaveService() {
    209         super(TAG);
    210         setIntentRedelivery(true);
    211         mMainHandler = new Handler(Looper.getMainLooper());
    212     }
    213 
    214     @Override
    215     public void onCreate() {
    216         super.onCreate();
    217         mGroupsDao = new GroupsDaoImpl(this);
    218         mSimContactDao = SimContactDao.create(this);
    219     }
    220 
    221     public static void registerListener(Listener listener) {
    222         if (!(listener instanceof Activity)) {
    223             throw new ClassCastException("Only activities can be registered to"
    224                     + " receive callback from " + ContactSaveService.class.getName());
    225         }
    226         sListeners.add(0, listener);
    227     }
    228 
    229     public static boolean canUndo(Intent resultIntent) {
    230         return resultIntent.hasExtra(EXTRA_UNDO_DATA);
    231     }
    232 
    233     public static void unregisterListener(Listener listener) {
    234         sListeners.remove(listener);
    235     }
    236 
    237     public static State getState() {
    238         return sState;
    239     }
    240 
    241     private void notifyStateChanged() {
    242         LocalBroadcastManager.getInstance(this)
    243                 .sendBroadcast(new Intent(BROADCAST_SERVICE_STATE_CHANGED));
    244     }
    245 
    246     /**
    247      * Returns true if the ContactSaveService was started successfully and false if an exception
    248      * was thrown and a Toast error message was displayed.
    249      */
    250     public static boolean startService(Context context, Intent intent, int saveMode) {
    251         try {
    252             context.startService(intent);
    253         } catch (Exception exception) {
    254             final int resId;
    255             switch (saveMode) {
    256                 case ContactEditorActivity.ContactEditor.SaveMode.SPLIT:
    257                     resId = R.string.contactUnlinkErrorToast;
    258                     break;
    259                 case ContactEditorActivity.ContactEditor.SaveMode.RELOAD:
    260                     resId = R.string.contactJoinErrorToast;
    261                     break;
    262                 case ContactEditorActivity.ContactEditor.SaveMode.CLOSE:
    263                     resId = R.string.contactSavedErrorToast;
    264                     break;
    265                 default:
    266                     resId = R.string.contactGenericErrorToast;
    267             }
    268             Toast.makeText(context, resId, Toast.LENGTH_SHORT).show();
    269             return false;
    270         }
    271         return true;
    272     }
    273 
    274     /**
    275      * Utility method that starts service and handles exception.
    276      */
    277     public static void startService(Context context, Intent intent) {
    278         try {
    279             context.startService(intent);
    280         } catch (Exception exception) {
    281             Toast.makeText(context, R.string.contactGenericErrorToast, Toast.LENGTH_SHORT).show();
    282         }
    283     }
    284 
    285     @Override
    286     public Object getSystemService(String name) {
    287         Object service = super.getSystemService(name);
    288         if (service != null) {
    289             return service;
    290         }
    291 
    292         return getApplicationContext().getSystemService(name);
    293     }
    294 
    295     // Parent classes Javadoc says not to override this method but we're doing it just to update
    296     // our state which should be OK since we're still doing the work in onHandleIntent
    297     @Override
    298     public int onStartCommand(Intent intent, int flags, int startId) {
    299         sState.onStart(intent);
    300         notifyStateChanged();
    301         return super.onStartCommand(intent, flags, startId);
    302     }
    303 
    304     @Override
    305     protected void onHandleIntent(final Intent intent) {
    306         if (intent == null) {
    307             if (Log.isLoggable(TAG, Log.DEBUG)) {
    308                 Log.d(TAG, "onHandleIntent: could not handle null intent");
    309             }
    310             return;
    311         }
    312         if (!PermissionsUtil.hasPermission(this, WRITE_CONTACTS)) {
    313             Log.w(TAG, "No WRITE_CONTACTS permission, unable to write to CP2");
    314             // TODO: add more specific error string such as "Turn on Contacts
    315             // permission to update your contacts"
    316             showToast(R.string.contactSavedErrorToast);
    317             return;
    318         }
    319 
    320         // Call an appropriate method. If we're sure it affects how incoming phone calls are
    321         // handled, then notify the fact to in-call screen.
    322         String action = intent.getAction();
    323         if (ACTION_NEW_RAW_CONTACT.equals(action)) {
    324             createRawContact(intent);
    325         } else if (ACTION_SAVE_CONTACT.equals(action)) {
    326             saveContact(intent);
    327         } else if (ACTION_CREATE_GROUP.equals(action)) {
    328             createGroup(intent);
    329         } else if (ACTION_RENAME_GROUP.equals(action)) {
    330             renameGroup(intent);
    331         } else if (ACTION_DELETE_GROUP.equals(action)) {
    332             deleteGroup(intent);
    333         } else if (ACTION_UPDATE_GROUP.equals(action)) {
    334             updateGroup(intent);
    335         } else if (ACTION_SET_STARRED.equals(action)) {
    336             setStarred(intent);
    337         } else if (ACTION_SET_SUPER_PRIMARY.equals(action)) {
    338             setSuperPrimary(intent);
    339         } else if (ACTION_CLEAR_PRIMARY.equals(action)) {
    340             clearPrimary(intent);
    341         } else if (ACTION_DELETE_MULTIPLE_CONTACTS.equals(action)) {
    342             deleteMultipleContacts(intent);
    343         } else if (ACTION_DELETE_CONTACT.equals(action)) {
    344             deleteContact(intent);
    345         } else if (ACTION_SPLIT_CONTACT.equals(action)) {
    346             splitContact(intent);
    347         } else if (ACTION_JOIN_CONTACTS.equals(action)) {
    348             joinContacts(intent);
    349         } else if (ACTION_JOIN_SEVERAL_CONTACTS.equals(action)) {
    350             joinSeveralContacts(intent);
    351         } else if (ACTION_SET_SEND_TO_VOICEMAIL.equals(action)) {
    352             setSendToVoicemail(intent);
    353         } else if (ACTION_SET_RINGTONE.equals(action)) {
    354             setRingtone(intent);
    355         } else if (ACTION_UNDO.equals(action)) {
    356             undo(intent);
    357         } else if (ACTION_SLEEP.equals(action)) {
    358             sleepForDebugging(intent);
    359         }
    360 
    361         sState.onFinish(intent);
    362         notifyStateChanged();
    363     }
    364 
    365     /**
    366      * Creates an intent that can be sent to this service to create a new raw contact
    367      * using data presented as a set of ContentValues.
    368      */
    369     public static Intent createNewRawContactIntent(Context context,
    370             ArrayList<ContentValues> values, AccountWithDataSet account,
    371             Class<? extends Activity> callbackActivity, String callbackAction) {
    372         Intent serviceIntent = new Intent(
    373                 context, ContactSaveService.class);
    374         serviceIntent.setAction(ContactSaveService.ACTION_NEW_RAW_CONTACT);
    375         if (account != null) {
    376             serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
    377             serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
    378             serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
    379         }
    380         serviceIntent.putParcelableArrayListExtra(
    381                 ContactSaveService.EXTRA_CONTENT_VALUES, values);
    382 
    383         // Callback intent will be invoked by the service once the new contact is
    384         // created.  The service will put the URI of the new contact as "data" on
    385         // the callback intent.
    386         Intent callbackIntent = new Intent(context, callbackActivity);
    387         callbackIntent.setAction(callbackAction);
    388         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    389         return serviceIntent;
    390     }
    391 
    392     private void createRawContact(Intent intent) {
    393         String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
    394         String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
    395         String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
    396         List<ContentValues> valueList = intent.getParcelableArrayListExtra(EXTRA_CONTENT_VALUES);
    397         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    398 
    399         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
    400         operations.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
    401                 .withValue(RawContacts.ACCOUNT_NAME, accountName)
    402                 .withValue(RawContacts.ACCOUNT_TYPE, accountType)
    403                 .withValue(RawContacts.DATA_SET, dataSet)
    404                 .build());
    405 
    406         int size = valueList.size();
    407         for (int i = 0; i < size; i++) {
    408             ContentValues values = valueList.get(i);
    409             values.keySet().retainAll(ALLOWED_DATA_COLUMNS);
    410             operations.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
    411                     .withValueBackReference(Data.RAW_CONTACT_ID, 0)
    412                     .withValues(values)
    413                     .build());
    414         }
    415 
    416         ContentResolver resolver = getContentResolver();
    417         ContentProviderResult[] results;
    418         try {
    419             results = resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    420         } catch (Exception e) {
    421             throw new RuntimeException("Failed to store new contact", e);
    422         }
    423 
    424         Uri rawContactUri = results[0].uri;
    425         callbackIntent.setData(RawContacts.getContactLookupUri(resolver, rawContactUri));
    426 
    427         deliverCallback(callbackIntent);
    428     }
    429 
    430     /**
    431      * Creates an intent that can be sent to this service to create a new raw contact
    432      * using data presented as a set of ContentValues.
    433      * This variant is more convenient to use when there is only one photo that can
    434      * possibly be updated, as in the Contact Details screen.
    435      * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
    436      * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
    437      */
    438     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
    439             String saveModeExtraKey, int saveMode, boolean isProfile,
    440             Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
    441             Uri updatedPhotoPath) {
    442         Bundle bundle = new Bundle();
    443         bundle.putParcelable(String.valueOf(rawContactId), updatedPhotoPath);
    444         return createSaveContactIntent(context, state, saveModeExtraKey, saveMode, isProfile,
    445                 callbackActivity, callbackAction, bundle,
    446                 /* joinContactIdExtraKey */ null, /* joinContactId */ null);
    447     }
    448 
    449     /**
    450      * Creates an intent that can be sent to this service to create a new raw contact
    451      * using data presented as a set of ContentValues.
    452      * This variant is used when multiple contacts' photos may be updated, as in the
    453      * Contact Editor.
    454      *
    455      * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
    456      * @param joinContactIdExtraKey the key used to pass the joinContactId in the callback intent.
    457      * @param joinContactId the raw contact ID to join to the contact after doing the save.
    458      */
    459     public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
    460             String saveModeExtraKey, int saveMode, boolean isProfile,
    461             Class<? extends Activity> callbackActivity, String callbackAction,
    462             Bundle updatedPhotos, String joinContactIdExtraKey, Long joinContactId) {
    463         Intent serviceIntent = new Intent(
    464                 context, ContactSaveService.class);
    465         serviceIntent.setAction(ContactSaveService.ACTION_SAVE_CONTACT);
    466         serviceIntent.putExtra(EXTRA_CONTACT_STATE, (Parcelable) state);
    467         serviceIntent.putExtra(EXTRA_SAVE_IS_PROFILE, isProfile);
    468         serviceIntent.putExtra(EXTRA_SAVE_MODE, saveMode);
    469 
    470         if (updatedPhotos != null) {
    471             serviceIntent.putExtra(EXTRA_UPDATED_PHOTOS, (Parcelable) updatedPhotos);
    472         }
    473 
    474         if (callbackActivity != null) {
    475             // Callback intent will be invoked by the service once the contact is
    476             // saved.  The service will put the URI of the new contact as "data" on
    477             // the callback intent.
    478             Intent callbackIntent = new Intent(context, callbackActivity);
    479             callbackIntent.putExtra(saveModeExtraKey, saveMode);
    480             if (joinContactIdExtraKey != null && joinContactId != null) {
    481                 callbackIntent.putExtra(joinContactIdExtraKey, joinContactId);
    482             }
    483             callbackIntent.setAction(callbackAction);
    484             serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    485         }
    486         return serviceIntent;
    487     }
    488 
    489     private void saveContact(Intent intent) {
    490         RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
    491         boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
    492         Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
    493 
    494         if (state == null) {
    495             Log.e(TAG, "Invalid arguments for saveContact request");
    496             return;
    497         }
    498 
    499         int saveMode = intent.getIntExtra(EXTRA_SAVE_MODE, -1);
    500         // Trim any empty fields, and RawContacts, before persisting
    501         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
    502         RawContactModifier.trimEmpty(state, accountTypes);
    503 
    504         Uri lookupUri = null;
    505 
    506         final ContentResolver resolver = getContentResolver();
    507 
    508         boolean succeeded = false;
    509 
    510         // Keep track of the id of a newly raw-contact (if any... there can be at most one).
    511         long insertedRawContactId = -1;
    512 
    513         // Attempt to persist changes
    514         int tries = 0;
    515         while (tries++ < PERSIST_TRIES) {
    516             try {
    517                 // Build operations and try applying
    518                 final ArrayList<CPOWrapper> diffWrapper = state.buildDiffWrapper();
    519 
    520                 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    521 
    522                 for (CPOWrapper cpoWrapper : diffWrapper) {
    523                     diff.add(cpoWrapper.getOperation());
    524                 }
    525 
    526                 if (DEBUG) {
    527                     Log.v(TAG, "Content Provider Operations:");
    528                     for (ContentProviderOperation operation : diff) {
    529                         Log.v(TAG, operation.toString());
    530                     }
    531                 }
    532 
    533                 int numberProcessed = 0;
    534                 boolean batchFailed = false;
    535                 final ContentProviderResult[] results = new ContentProviderResult[diff.size()];
    536                 while (numberProcessed < diff.size()) {
    537                     final int subsetCount = applyDiffSubset(diff, numberProcessed, results, resolver);
    538                     if (subsetCount == -1) {
    539                         Log.w(TAG, "Resolver.applyBatch failed in saveContacts");
    540                         batchFailed = true;
    541                         break;
    542                     } else {
    543                         numberProcessed += subsetCount;
    544                     }
    545                 }
    546 
    547                 if (batchFailed) {
    548                     // Retry save
    549                     continue;
    550                 }
    551 
    552                 final long rawContactId = getRawContactId(state, diffWrapper, results);
    553                 if (rawContactId == -1) {
    554                     throw new IllegalStateException("Could not determine RawContact ID after save");
    555                 }
    556                 // We don't have to check to see if the value is still -1.  If we reach here,
    557                 // the previous loop iteration didn't succeed, so any ID that we obtained is bogus.
    558                 insertedRawContactId = getInsertedRawContactId(diffWrapper, results);
    559                 if (isProfile) {
    560                     // Since the profile supports local raw contacts, which may have been completely
    561                     // removed if all information was removed, we need to do a special query to
    562                     // get the lookup URI for the profile contact (if it still exists).
    563                     Cursor c = resolver.query(Profile.CONTENT_URI,
    564                             new String[] {Contacts._ID, Contacts.LOOKUP_KEY},
    565                             null, null, null);
    566                     if (c == null) {
    567                         continue;
    568                     }
    569                     try {
    570                         if (c.moveToFirst()) {
    571                             final long contactId = c.getLong(0);
    572                             final String lookupKey = c.getString(1);
    573                             lookupUri = Contacts.getLookupUri(contactId, lookupKey);
    574                         }
    575                     } finally {
    576                         c.close();
    577                     }
    578                 } else {
    579                     final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
    580                                     rawContactId);
    581                     lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
    582                 }
    583                 if (lookupUri != null && Log.isLoggable(TAG, Log.VERBOSE)) {
    584                     Log.v(TAG, "Saved contact. New URI: " + lookupUri);
    585                 }
    586 
    587                 // We can change this back to false later, if we fail to save the contact photo.
    588                 succeeded = true;
    589                 break;
    590 
    591             } catch (RemoteException e) {
    592                 // Something went wrong, bail without success
    593                 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
    594                 break;
    595 
    596             } catch (IllegalArgumentException e) {
    597                 // This is thrown by applyBatch on malformed requests
    598                 FeedbackHelper.sendFeedback(this, TAG, "Problem persisting user edits", e);
    599                 showToast(R.string.contactSavedErrorToast);
    600                 break;
    601 
    602             } catch (OperationApplicationException e) {
    603                 // Version consistency failed, re-parent change and try again
    604                 Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
    605                 final StringBuilder sb = new StringBuilder(RawContacts._ID + " IN(");
    606                 boolean first = true;
    607                 final int count = state.size();
    608                 for (int i = 0; i < count; i++) {
    609                     Long rawContactId = state.getRawContactId(i);
    610                     if (rawContactId != null && rawContactId != -1) {
    611                         if (!first) {
    612                             sb.append(',');
    613                         }
    614                         sb.append(rawContactId);
    615                         first = false;
    616                     }
    617                 }
    618                 sb.append(")");
    619 
    620                 if (first) {
    621                     throw new IllegalStateException(
    622                             "Version consistency failed for a new contact", e);
    623                 }
    624 
    625                 final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
    626                         isProfile
    627                                 ? RawContactsEntity.PROFILE_CONTENT_URI
    628                                 : RawContactsEntity.CONTENT_URI,
    629                         resolver, sb.toString(), null, null);
    630                 state = RawContactDeltaList.mergeAfter(newState, state);
    631 
    632                 // Update the new state to use profile URIs if appropriate.
    633                 if (isProfile) {
    634                     for (RawContactDelta delta : state) {
    635                         delta.setProfileQueryUri();
    636                     }
    637                 }
    638             }
    639         }
    640 
    641         // Now save any updated photos.  We do this at the end to ensure that
    642         // the ContactProvider already knows about newly-created contacts.
    643         if (updatedPhotos != null) {
    644             for (String key : updatedPhotos.keySet()) {
    645                 Uri photoUri = updatedPhotos.getParcelable(key);
    646                 long rawContactId = Long.parseLong(key);
    647 
    648                 // If the raw-contact ID is negative, we are saving a new raw-contact;
    649                 // replace the bogus ID with the new one that we actually saved the contact at.
    650                 if (rawContactId < 0) {
    651                     rawContactId = insertedRawContactId;
    652                 }
    653 
    654                 // If the save failed, insertedRawContactId will be -1
    655                 if (rawContactId < 0 || !saveUpdatedPhoto(rawContactId, photoUri, saveMode)) {
    656                     succeeded = false;
    657                 }
    658             }
    659         }
    660 
    661         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    662         if (callbackIntent != null) {
    663             if (succeeded) {
    664                 // Mark the intent to indicate that the save was successful (even if the lookup URI
    665                 // is now null).  For local contacts or the local profile, it's possible that the
    666                 // save triggered removal of the contact, so no lookup URI would exist..
    667                 callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
    668             }
    669             callbackIntent.setData(lookupUri);
    670             deliverCallback(callbackIntent);
    671         }
    672     }
    673 
    674     /**
    675      * Splits "diff" into subsets based on "MAX_CONTACTS_PROVIDER_BATCH_SIZE", applies each of the
    676      * subsets, adds the returned array to "results".
    677      *
    678      * @return the size of the array, if not null; -1 when the array is null.
    679      */
    680     private int applyDiffSubset(ArrayList<ContentProviderOperation> diff, int offset,
    681             ContentProviderResult[] results, ContentResolver resolver)
    682             throws RemoteException, OperationApplicationException {
    683         final int subsetCount = Math.min(diff.size() - offset, MAX_CONTACTS_PROVIDER_BATCH_SIZE);
    684         final ArrayList<ContentProviderOperation> subset = new ArrayList<>();
    685         subset.addAll(diff.subList(offset, offset + subsetCount));
    686         final ContentProviderResult[] subsetResult = resolver.applyBatch(ContactsContract
    687                 .AUTHORITY, subset);
    688         if (subsetResult == null || (offset + subsetResult.length) > results.length) {
    689             return -1;
    690         }
    691         for (ContentProviderResult c : subsetResult) {
    692             results[offset++] = c;
    693         }
    694         return subsetResult.length;
    695     }
    696 
    697     /**
    698      * Save updated photo for the specified raw-contact.
    699      * @return true for success, false for failure
    700      */
    701     private boolean saveUpdatedPhoto(long rawContactId, Uri photoUri, int saveMode) {
    702         final Uri outputUri = Uri.withAppendedPath(
    703                 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
    704                 RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
    705 
    706         return ContactPhotoUtils.savePhotoFromUriToUri(this, photoUri, outputUri, (saveMode == 0));
    707     }
    708 
    709     /**
    710      * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
    711      */
    712     private long getRawContactId(RawContactDeltaList state,
    713             final ArrayList<CPOWrapper> diffWrapper,
    714             final ContentProviderResult[] results) {
    715         long existingRawContactId = state.findRawContactId();
    716         if (existingRawContactId != -1) {
    717             return existingRawContactId;
    718         }
    719 
    720         return getInsertedRawContactId(diffWrapper, results);
    721     }
    722 
    723     /**
    724      * Find the ID of a newly-inserted raw-contact.  If none exists, return -1.
    725      */
    726     private long getInsertedRawContactId(
    727             final ArrayList<CPOWrapper> diffWrapper, final ContentProviderResult[] results) {
    728         if (results == null) {
    729             return -1;
    730         }
    731         final int diffSize = diffWrapper.size();
    732         final int numResults = results.length;
    733         for (int i = 0; i < diffSize && i < numResults; i++) {
    734             final CPOWrapper cpoWrapper = diffWrapper.get(i);
    735             final boolean isInsert = CompatUtils.isInsertCompat(cpoWrapper);
    736             if (isInsert && cpoWrapper.getOperation().getUri().getEncodedPath().contains(
    737                     RawContacts.CONTENT_URI.getEncodedPath())) {
    738                 return ContentUris.parseId(results[i].uri);
    739             }
    740         }
    741         return -1;
    742     }
    743 
    744     /**
    745      * Creates an intent that can be sent to this service to create a new group as
    746      * well as add new members at the same time.
    747      *
    748      * @param context of the application
    749      * @param account in which the group should be created
    750      * @param label is the name of the group (cannot be null)
    751      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    752      *            should be added to the group
    753      * @param callbackActivity is the activity to send the callback intent to
    754      * @param callbackAction is the intent action for the callback intent
    755      */
    756     public static Intent createNewGroupIntent(Context context, AccountWithDataSet account,
    757             String label, long[] rawContactsToAdd, Class<? extends Activity> callbackActivity,
    758             String callbackAction) {
    759         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    760         serviceIntent.setAction(ContactSaveService.ACTION_CREATE_GROUP);
    761         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_TYPE, account.type);
    762         serviceIntent.putExtra(ContactSaveService.EXTRA_ACCOUNT_NAME, account.name);
    763         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_SET, account.dataSet);
    764         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, label);
    765         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    766 
    767         // Callback intent will be invoked by the service once the new group is
    768         // created.
    769         Intent callbackIntent = new Intent(context, callbackActivity);
    770         callbackIntent.setAction(callbackAction);
    771         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    772 
    773         return serviceIntent;
    774     }
    775 
    776     private void createGroup(Intent intent) {
    777         String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
    778         String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
    779         String dataSet = intent.getStringExtra(EXTRA_DATA_SET);
    780         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    781         final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    782 
    783         // Create the new group
    784         final Uri groupUri = mGroupsDao.create(label,
    785                 new AccountWithDataSet(accountName, accountType, dataSet));
    786         final ContentResolver resolver = getContentResolver();
    787 
    788         // If there's no URI, then the insertion failed. Abort early because group members can't be
    789         // added if the group doesn't exist
    790         if (groupUri == null) {
    791             Log.e(TAG, "Couldn't create group with label " + label);
    792             return;
    793         }
    794 
    795         // Add new group members
    796         addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));
    797 
    798         ContentValues values = new ContentValues();
    799         // TODO: Move this into the contact editor where it belongs. This needs to be integrated
    800         // with the way other intent extras that are passed to the
    801         // {@link ContactEditorActivity}.
    802         values.clear();
    803         values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    804         values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
    805 
    806         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    807         callbackIntent.setData(groupUri);
    808         // TODO: This can be taken out when the above TODO is addressed
    809         callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
    810         deliverCallback(callbackIntent);
    811     }
    812 
    813     /**
    814      * Creates an intent that can be sent to this service to rename a group.
    815      */
    816     public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
    817             Class<? extends Activity> callbackActivity, String callbackAction) {
    818         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    819         serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
    820         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    821         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    822 
    823         // Callback intent will be invoked by the service once the group is renamed.
    824         Intent callbackIntent = new Intent(context, callbackActivity);
    825         callbackIntent.setAction(callbackAction);
    826         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    827 
    828         return serviceIntent;
    829     }
    830 
    831     private void renameGroup(Intent intent) {
    832         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    833         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    834 
    835         if (groupId == -1) {
    836             Log.e(TAG, "Invalid arguments for renameGroup request");
    837             return;
    838         }
    839 
    840         ContentValues values = new ContentValues();
    841         values.put(Groups.TITLE, label);
    842         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    843         getContentResolver().update(groupUri, values, null, null);
    844 
    845         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    846         callbackIntent.setData(groupUri);
    847         deliverCallback(callbackIntent);
    848     }
    849 
    850     /**
    851      * Creates an intent that can be sent to this service to delete a group.
    852      */
    853     public static Intent createGroupDeletionIntent(Context context, long groupId) {
    854         final Intent serviceIntent = new Intent(context, ContactSaveService.class);
    855         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
    856         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    857 
    858         return serviceIntent;
    859     }
    860 
    861     private void deleteGroup(Intent intent) {
    862         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    863         if (groupId == -1) {
    864             Log.e(TAG, "Invalid arguments for deleteGroup request");
    865             return;
    866         }
    867         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    868 
    869         final Intent callbackIntent = new Intent(BROADCAST_GROUP_DELETED);
    870         final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
    871         callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
    872         callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);
    873 
    874         mGroupsDao.delete(groupUri);
    875 
    876         LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
    877     }
    878 
    879     public static Intent createUndoIntent(Context context, Intent resultIntent) {
    880         final Intent serviceIntent = new Intent(context, ContactSaveService.class);
    881         serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
    882         serviceIntent.putExtras(resultIntent);
    883         return serviceIntent;
    884     }
    885 
    886     private void undo(Intent intent) {
    887         final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
    888         if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
    889             mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
    890         }
    891     }
    892 
    893 
    894     /**
    895      * Creates an intent that can be sent to this service to rename a group as
    896      * well as add and remove members from the group.
    897      *
    898      * @param context of the application
    899      * @param groupId of the group that should be modified
    900      * @param newLabel is the updated name of the group (can be null if the name
    901      *            should not be updated)
    902      * @param rawContactsToAdd is an array of raw contact IDs for contacts that
    903      *            should be added to the group
    904      * @param rawContactsToRemove is an array of raw contact IDs for contacts
    905      *            that should be removed from the group
    906      * @param callbackActivity is the activity to send the callback intent to
    907      * @param callbackAction is the intent action for the callback intent
    908      */
    909     public static Intent createGroupUpdateIntent(Context context, long groupId, String newLabel,
    910             long[] rawContactsToAdd, long[] rawContactsToRemove,
    911             Class<? extends Activity> callbackActivity, String callbackAction) {
    912         Intent serviceIntent = new Intent(context, ContactSaveService.class);
    913         serviceIntent.setAction(ContactSaveService.ACTION_UPDATE_GROUP);
    914         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
    915         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
    916         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_ADD, rawContactsToAdd);
    917         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACTS_TO_REMOVE,
    918                 rawContactsToRemove);
    919 
    920         // Callback intent will be invoked by the service once the group is updated
    921         Intent callbackIntent = new Intent(context, callbackActivity);
    922         callbackIntent.setAction(callbackAction);
    923         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
    924 
    925         return serviceIntent;
    926     }
    927 
    928     private void updateGroup(Intent intent) {
    929         long groupId = intent.getLongExtra(EXTRA_GROUP_ID, -1);
    930         String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
    931         long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);
    932         long[] rawContactsToRemove = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_REMOVE);
    933 
    934         if (groupId == -1) {
    935             Log.e(TAG, "Invalid arguments for updateGroup request");
    936             return;
    937         }
    938 
    939         final ContentResolver resolver = getContentResolver();
    940         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
    941 
    942         // Update group name if necessary
    943         if (label != null) {
    944             ContentValues values = new ContentValues();
    945             values.put(Groups.TITLE, label);
    946             resolver.update(groupUri, values, null, null);
    947         }
    948 
    949         // Add and remove members if necessary
    950         addMembersToGroup(resolver, rawContactsToAdd, groupId);
    951         removeMembersFromGroup(resolver, rawContactsToRemove, groupId);
    952 
    953         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
    954         callbackIntent.setData(groupUri);
    955         deliverCallback(callbackIntent);
    956     }
    957 
    958     private void addMembersToGroup(ContentResolver resolver, long[] rawContactsToAdd,
    959             long groupId) {
    960         if (rawContactsToAdd == null) {
    961             return;
    962         }
    963         for (long rawContactId : rawContactsToAdd) {
    964             try {
    965                 final ArrayList<ContentProviderOperation> rawContactOperations =
    966                         new ArrayList<ContentProviderOperation>();
    967 
    968                 // Build an assert operation to ensure the contact is not already in the group
    969                 final ContentProviderOperation.Builder assertBuilder = ContentProviderOperation
    970                         .newAssertQuery(Data.CONTENT_URI);
    971                 assertBuilder.withSelection(Data.RAW_CONTACT_ID + "=? AND " +
    972                         Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
    973                         new String[] { String.valueOf(rawContactId),
    974                         GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
    975                 assertBuilder.withExpectedCount(0);
    976                 rawContactOperations.add(assertBuilder.build());
    977 
    978                 // Build an insert operation to add the contact to the group
    979                 final ContentProviderOperation.Builder insertBuilder = ContentProviderOperation
    980                         .newInsert(Data.CONTENT_URI);
    981                 insertBuilder.withValue(Data.RAW_CONTACT_ID, rawContactId);
    982                 insertBuilder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
    983                 insertBuilder.withValue(GroupMembership.GROUP_ROW_ID, groupId);
    984                 rawContactOperations.add(insertBuilder.build());
    985 
    986                 if (DEBUG) {
    987                     for (ContentProviderOperation operation : rawContactOperations) {
    988                         Log.v(TAG, operation.toString());
    989                     }
    990                 }
    991 
    992                 // Apply batch
    993                 if (!rawContactOperations.isEmpty()) {
    994                     resolver.applyBatch(ContactsContract.AUTHORITY, rawContactOperations);
    995                 }
    996             } catch (RemoteException e) {
    997                 // Something went wrong, bail without success
    998                 FeedbackHelper.sendFeedback(this, TAG,
    999                         "Problem persisting user edits for raw contact ID " +
   1000                                 String.valueOf(rawContactId), e);
   1001             } catch (OperationApplicationException e) {
   1002                 // The assert could have failed because the contact is already in the group,
   1003                 // just continue to the next contact
   1004                 FeedbackHelper.sendFeedback(this, TAG,
   1005                         "Assert failed in adding raw contact ID " +
   1006                                 String.valueOf(rawContactId) + ". Already exists in group " +
   1007                                 String.valueOf(groupId), e);
   1008             }
   1009         }
   1010     }
   1011 
   1012     private static void removeMembersFromGroup(ContentResolver resolver, long[] rawContactsToRemove,
   1013             long groupId) {
   1014         if (rawContactsToRemove == null) {
   1015             return;
   1016         }
   1017         for (long rawContactId : rawContactsToRemove) {
   1018             // Apply the delete operation on the data row for the given raw contact's
   1019             // membership in the given group. If no contact matches the provided selection, then
   1020             // nothing will be done. Just continue to the next contact.
   1021             resolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=? AND " +
   1022                     Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
   1023                     new String[] { String.valueOf(rawContactId),
   1024                     GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId)});
   1025         }
   1026     }
   1027 
   1028     /**
   1029      * Creates an intent that can be sent to this service to star or un-star a contact.
   1030      */
   1031     public static Intent createSetStarredIntent(Context context, Uri contactUri, boolean value) {
   1032         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1033         serviceIntent.setAction(ContactSaveService.ACTION_SET_STARRED);
   1034         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
   1035         serviceIntent.putExtra(ContactSaveService.EXTRA_STARRED_FLAG, value);
   1036 
   1037         return serviceIntent;
   1038     }
   1039 
   1040     private void setStarred(Intent intent) {
   1041         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
   1042         boolean value = intent.getBooleanExtra(EXTRA_STARRED_FLAG, false);
   1043         if (contactUri == null) {
   1044             Log.e(TAG, "Invalid arguments for setStarred request");
   1045             return;
   1046         }
   1047 
   1048         final ContentValues values = new ContentValues(1);
   1049         values.put(Contacts.STARRED, value);
   1050         getContentResolver().update(contactUri, values, null, null);
   1051 
   1052         // Undemote the contact if necessary
   1053         final Cursor c = getContentResolver().query(contactUri, new String[] {Contacts._ID},
   1054                 null, null, null);
   1055         if (c == null) {
   1056             return;
   1057         }
   1058         try {
   1059             if (c.moveToFirst()) {
   1060                 final long id = c.getLong(0);
   1061 
   1062                 // Don't bother undemoting if this contact is the user's profile.
   1063                 if (id < Profile.MIN_ID) {
   1064                     PinnedPositionsCompat.undemote(getContentResolver(), id);
   1065                 }
   1066             }
   1067         } finally {
   1068             c.close();
   1069         }
   1070     }
   1071 
   1072     /**
   1073      * Creates an intent that can be sent to this service to set the redirect to voicemail.
   1074      */
   1075     public static Intent createSetSendToVoicemail(Context context, Uri contactUri,
   1076             boolean value) {
   1077         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1078         serviceIntent.setAction(ContactSaveService.ACTION_SET_SEND_TO_VOICEMAIL);
   1079         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
   1080         serviceIntent.putExtra(ContactSaveService.EXTRA_SEND_TO_VOICEMAIL_FLAG, value);
   1081 
   1082         return serviceIntent;
   1083     }
   1084 
   1085     private void setSendToVoicemail(Intent intent) {
   1086         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
   1087         boolean value = intent.getBooleanExtra(EXTRA_SEND_TO_VOICEMAIL_FLAG, false);
   1088         if (contactUri == null) {
   1089             Log.e(TAG, "Invalid arguments for setRedirectToVoicemail");
   1090             return;
   1091         }
   1092 
   1093         final ContentValues values = new ContentValues(1);
   1094         values.put(Contacts.SEND_TO_VOICEMAIL, value);
   1095         getContentResolver().update(contactUri, values, null, null);
   1096     }
   1097 
   1098     /**
   1099      * Creates an intent that can be sent to this service to save the contact's ringtone.
   1100      */
   1101     public static Intent createSetRingtone(Context context, Uri contactUri,
   1102             String value) {
   1103         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1104         serviceIntent.setAction(ContactSaveService.ACTION_SET_RINGTONE);
   1105         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
   1106         serviceIntent.putExtra(ContactSaveService.EXTRA_CUSTOM_RINGTONE, value);
   1107 
   1108         return serviceIntent;
   1109     }
   1110 
   1111     private void setRingtone(Intent intent) {
   1112         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
   1113         String value = intent.getStringExtra(EXTRA_CUSTOM_RINGTONE);
   1114         if (contactUri == null) {
   1115             Log.e(TAG, "Invalid arguments for setRingtone");
   1116             return;
   1117         }
   1118         ContentValues values = new ContentValues(1);
   1119         values.put(Contacts.CUSTOM_RINGTONE, value);
   1120         getContentResolver().update(contactUri, values, null, null);
   1121     }
   1122 
   1123     /**
   1124      * Creates an intent that sets the selected data item as super primary (default)
   1125      */
   1126     public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
   1127         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1128         serviceIntent.setAction(ContactSaveService.ACTION_SET_SUPER_PRIMARY);
   1129         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
   1130         return serviceIntent;
   1131     }
   1132 
   1133     private void setSuperPrimary(Intent intent) {
   1134         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
   1135         if (dataId == -1) {
   1136             Log.e(TAG, "Invalid arguments for setSuperPrimary request");
   1137             return;
   1138         }
   1139 
   1140         ContactUpdateUtils.setSuperPrimary(this, dataId);
   1141     }
   1142 
   1143     /**
   1144      * Creates an intent that clears the primary flag of all data items that belong to the same
   1145      * raw_contact as the given data item. Will only clear, if the data item was primary before
   1146      * this call
   1147      */
   1148     public static Intent createClearPrimaryIntent(Context context, long dataId) {
   1149         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1150         serviceIntent.setAction(ContactSaveService.ACTION_CLEAR_PRIMARY);
   1151         serviceIntent.putExtra(ContactSaveService.EXTRA_DATA_ID, dataId);
   1152         return serviceIntent;
   1153     }
   1154 
   1155     private void clearPrimary(Intent intent) {
   1156         long dataId = intent.getLongExtra(EXTRA_DATA_ID, -1);
   1157         if (dataId == -1) {
   1158             Log.e(TAG, "Invalid arguments for clearPrimary request");
   1159             return;
   1160         }
   1161 
   1162         // Update the primary values in the data record.
   1163         ContentValues values = new ContentValues(1);
   1164         values.put(Data.IS_SUPER_PRIMARY, 0);
   1165         values.put(Data.IS_PRIMARY, 0);
   1166 
   1167         getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
   1168                 values, null, null);
   1169     }
   1170 
   1171     /**
   1172      * Creates an intent that can be sent to this service to delete a contact.
   1173      */
   1174     public static Intent createDeleteContactIntent(Context context, Uri contactUri) {
   1175         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1176         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_CONTACT);
   1177         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_URI, contactUri);
   1178         return serviceIntent;
   1179     }
   1180 
   1181     /**
   1182      * Creates an intent that can be sent to this service to delete multiple contacts.
   1183      */
   1184     public static Intent createDeleteMultipleContactsIntent(Context context,
   1185             long[] contactIds, final String[] names) {
   1186         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1187         serviceIntent.setAction(ContactSaveService.ACTION_DELETE_MULTIPLE_CONTACTS);
   1188         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
   1189         serviceIntent.putExtra(ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY, names);
   1190         return serviceIntent;
   1191     }
   1192 
   1193     private void deleteContact(Intent intent) {
   1194         Uri contactUri = intent.getParcelableExtra(EXTRA_CONTACT_URI);
   1195         if (contactUri == null) {
   1196             Log.e(TAG, "Invalid arguments for deleteContact request");
   1197             return;
   1198         }
   1199 
   1200         getContentResolver().delete(contactUri, null, null);
   1201     }
   1202 
   1203     private void deleteMultipleContacts(Intent intent) {
   1204         final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
   1205         if (contactIds == null) {
   1206             Log.e(TAG, "Invalid arguments for deleteMultipleContacts request");
   1207             return;
   1208         }
   1209         for (long contactId : contactIds) {
   1210             final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
   1211             getContentResolver().delete(contactUri, null, null);
   1212         }
   1213         final String[] names = intent.getStringArrayExtra(
   1214                 ContactSaveService.EXTRA_DISPLAY_NAME_ARRAY);
   1215         final String deleteToastMessage;
   1216         if (contactIds.length != names.length || names.length == 0) {
   1217             deleteToastMessage = getResources().getQuantityString(
   1218                     R.plurals.contacts_deleted_toast, contactIds.length);
   1219         } else if (names.length == 1) {
   1220             deleteToastMessage = getResources().getString(
   1221                     R.string.contacts_deleted_one_named_toast, names);
   1222         } else if (names.length == 2) {
   1223             deleteToastMessage = getResources().getString(
   1224                     R.string.contacts_deleted_two_named_toast, names);
   1225         } else {
   1226             deleteToastMessage = getResources().getString(
   1227                     R.string.contacts_deleted_many_named_toast, names);
   1228         }
   1229 
   1230         mMainHandler.post(new Runnable() {
   1231             @Override
   1232             public void run() {
   1233                 Toast.makeText(ContactSaveService.this, deleteToastMessage, Toast.LENGTH_LONG)
   1234                         .show();
   1235             }
   1236         });
   1237     }
   1238 
   1239     /**
   1240      * Creates an intent that can be sent to this service to split a contact into it's constituent
   1241      * pieces. This will set the raw contact ids to {@link AggregationExceptions#TYPE_AUTOMATIC} so
   1242      * they may be re-merged by the auto-aggregator.
   1243      */
   1244     public static Intent createSplitContactIntent(Context context, long[][] rawContactIds,
   1245             ResultReceiver receiver) {
   1246         final Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1247         serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
   1248         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
   1249         serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
   1250         return serviceIntent;
   1251     }
   1252 
   1253     /**
   1254      * Creates an intent that can be sent to this service to split a contact into it's constituent
   1255      * pieces. This will explicitly set the raw contact ids to
   1256      * {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
   1257      */
   1258     public static Intent createHardSplitContactIntent(Context context, long[][] rawContactIds) {
   1259         final Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1260         serviceIntent.setAction(ContactSaveService.ACTION_SPLIT_CONTACT);
   1261         serviceIntent.putExtra(ContactSaveService.EXTRA_RAW_CONTACT_IDS, rawContactIds);
   1262         serviceIntent.putExtra(ContactSaveService.EXTRA_HARD_SPLIT, true);
   1263         return serviceIntent;
   1264     }
   1265 
   1266     private void splitContact(Intent intent) {
   1267         final long rawContactIds[][] = (long[][]) intent
   1268                 .getSerializableExtra(EXTRA_RAW_CONTACT_IDS);
   1269         final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
   1270         final boolean hardSplit = intent.getBooleanExtra(EXTRA_HARD_SPLIT, false);
   1271         if (rawContactIds == null) {
   1272             Log.e(TAG, "Invalid argument for splitContact request");
   1273             if (receiver != null) {
   1274                 receiver.send(BAD_ARGUMENTS, new Bundle());
   1275             }
   1276             return;
   1277         }
   1278         final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
   1279         final ContentResolver resolver = getContentResolver();
   1280         final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
   1281         for (int i = 0; i < rawContactIds.length; i++) {
   1282             for (int j = 0; j < rawContactIds.length; j++) {
   1283                 if (i != j) {
   1284                     if (!buildSplitTwoContacts(operations, rawContactIds[i], rawContactIds[j],
   1285                             hardSplit)) {
   1286                         if (receiver != null) {
   1287                             receiver.send(CP2_ERROR, new Bundle());
   1288                             return;
   1289                         }
   1290                     }
   1291                 }
   1292             }
   1293         }
   1294         if (operations.size() > 0 && !applyOperations(resolver, operations)) {
   1295             if (receiver != null) {
   1296                 receiver.send(CP2_ERROR, new Bundle());
   1297             }
   1298             return;
   1299         }
   1300         LocalBroadcastManager.getInstance(this)
   1301                 .sendBroadcast(new Intent(BROADCAST_UNLINK_COMPLETE));
   1302         if (receiver != null) {
   1303             receiver.send(CONTACTS_SPLIT, new Bundle());
   1304         } else {
   1305             showToast(R.string.contactUnlinkedToast);
   1306         }
   1307     }
   1308 
   1309     /**
   1310      * Insert aggregation exception ContentProviderOperations between {@param rawContactIds1}
   1311      * and {@param rawContactIds2} to {@param operations}.
   1312      * @return false if an error occurred, true otherwise.
   1313      */
   1314     private boolean buildSplitTwoContacts(ArrayList<ContentProviderOperation> operations,
   1315             long[] rawContactIds1, long[] rawContactIds2, boolean hardSplit) {
   1316         if (rawContactIds1 == null || rawContactIds2 == null) {
   1317             Log.e(TAG, "Invalid arguments for splitContact request");
   1318             return false;
   1319         }
   1320         // For each pair of raw contacts, insert an aggregation exception
   1321         final ContentResolver resolver = getContentResolver();
   1322         // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
   1323         final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
   1324         for (int i = 0; i < rawContactIds1.length; i++) {
   1325             for (int j = 0; j < rawContactIds2.length; j++) {
   1326                 buildSplitContactDiff(operations, rawContactIds1[i], rawContactIds2[j], hardSplit);
   1327                 // Before we get to 500 we need to flush the operations list
   1328                 if (operations.size() > 0 && operations.size() % batchSize == 0) {
   1329                     if (!applyOperations(resolver, operations)) {
   1330                         return false;
   1331                     }
   1332                     operations.clear();
   1333                 }
   1334             }
   1335         }
   1336         return true;
   1337     }
   1338 
   1339     /**
   1340      * Creates an intent that can be sent to this service to join two contacts.
   1341      * The resulting contact uses the name from {@param contactId1} if possible.
   1342      */
   1343     public static Intent createJoinContactsIntent(Context context, long contactId1,
   1344             long contactId2, Class<? extends Activity> callbackActivity, String callbackAction) {
   1345         Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1346         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_CONTACTS);
   1347         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID1, contactId1);
   1348         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_ID2, contactId2);
   1349 
   1350         // Callback intent will be invoked by the service once the contacts are joined.
   1351         Intent callbackIntent = new Intent(context, callbackActivity);
   1352         callbackIntent.setAction(callbackAction);
   1353         serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
   1354 
   1355         return serviceIntent;
   1356     }
   1357 
   1358     /**
   1359      * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
   1360      * No special attention is paid to where the resulting contact's name is taken from.
   1361      */
   1362     public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds,
   1363             ResultReceiver receiver) {
   1364         final Intent serviceIntent = new Intent(context, ContactSaveService.class);
   1365         serviceIntent.setAction(ContactSaveService.ACTION_JOIN_SEVERAL_CONTACTS);
   1366         serviceIntent.putExtra(ContactSaveService.EXTRA_CONTACT_IDS, contactIds);
   1367         serviceIntent.putExtra(ContactSaveService.EXTRA_RESULT_RECEIVER, receiver);
   1368         return serviceIntent;
   1369     }
   1370 
   1371     /**
   1372      * Creates an intent to join all raw contacts inside {@param contactIds}'s contacts.
   1373      * No special attention is paid to where the resulting contact's name is taken from.
   1374      */
   1375     public static Intent createJoinSeveralContactsIntent(Context context, long[] contactIds) {
   1376         return createJoinSeveralContactsIntent(context, contactIds, /* receiver = */ null);
   1377     }
   1378 
   1379     private interface JoinContactQuery {
   1380         String[] PROJECTION = {
   1381                 RawContacts._ID,
   1382                 RawContacts.CONTACT_ID,
   1383                 RawContacts.DISPLAY_NAME_SOURCE,
   1384         };
   1385 
   1386         int _ID = 0;
   1387         int CONTACT_ID = 1;
   1388         int DISPLAY_NAME_SOURCE = 2;
   1389     }
   1390 
   1391     private interface ContactEntityQuery {
   1392         String[] PROJECTION = {
   1393                 Contacts.Entity.DATA_ID,
   1394                 Contacts.Entity.CONTACT_ID,
   1395                 Contacts.Entity.IS_SUPER_PRIMARY,
   1396         };
   1397         String SELECTION = Data.MIMETYPE + " = '" + StructuredName.CONTENT_ITEM_TYPE + "'" +
   1398                 " AND " + StructuredName.DISPLAY_NAME + "=" + Contacts.DISPLAY_NAME +
   1399                 " AND " + StructuredName.DISPLAY_NAME + " IS NOT NULL " +
   1400                 " AND " + StructuredName.DISPLAY_NAME + " != '' ";
   1401 
   1402         int DATA_ID = 0;
   1403         int CONTACT_ID = 1;
   1404         int IS_SUPER_PRIMARY = 2;
   1405     }
   1406 
   1407     private void joinSeveralContacts(Intent intent) {
   1408         final long[] contactIds = intent.getLongArrayExtra(EXTRA_CONTACT_IDS);
   1409 
   1410         final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
   1411 
   1412         // Load raw contact IDs for all contacts involved.
   1413         final long rawContactIds[] = getRawContactIdsForAggregation(contactIds);
   1414         final long[][] separatedRawContactIds = getSeparatedRawContactIds(contactIds);
   1415         if (rawContactIds == null) {
   1416             Log.e(TAG, "Invalid arguments for joinSeveralContacts request");
   1417             if (receiver != null) {
   1418                 receiver.send(BAD_ARGUMENTS, new Bundle());
   1419             }
   1420             return;
   1421         }
   1422 
   1423         // For each pair of raw contacts, insert an aggregation exception
   1424         final ContentResolver resolver = getContentResolver();
   1425         // The maximum number of operations per batch (aka yield point) is 500. See b/22480225
   1426         final int batchSize = MAX_CONTACTS_PROVIDER_BATCH_SIZE;
   1427         final ArrayList<ContentProviderOperation> operations = new ArrayList<>(batchSize);
   1428         for (int i = 0; i < rawContactIds.length; i++) {
   1429             for (int j = 0; j < rawContactIds.length; j++) {
   1430                 if (i != j) {
   1431                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
   1432                 }
   1433                 // Before we get to 500 we need to flush the operations list
   1434                 if (operations.size() > 0 && operations.size() % batchSize == 0) {
   1435                     if (!applyOperations(resolver, operations)) {
   1436                         if (receiver != null) {
   1437                             receiver.send(CP2_ERROR, new Bundle());
   1438                         }
   1439                         return;
   1440                     }
   1441                     operations.clear();
   1442                 }
   1443             }
   1444         }
   1445         if (operations.size() > 0 && !applyOperations(resolver, operations)) {
   1446             if (receiver != null) {
   1447                 receiver.send(CP2_ERROR, new Bundle());
   1448             }
   1449             return;
   1450         }
   1451 
   1452 
   1453         final String name = queryNameOfLinkedContacts(contactIds);
   1454         if (name != null) {
   1455             if (receiver != null) {
   1456                 final Bundle result = new Bundle();
   1457                 result.putSerializable(EXTRA_RAW_CONTACT_IDS, separatedRawContactIds);
   1458                 result.putString(EXTRA_DISPLAY_NAME, name);
   1459                 receiver.send(CONTACTS_LINKED, result);
   1460             } else {
   1461                 if (TextUtils.isEmpty(name)) {
   1462                     showToast(R.string.contactsJoinedMessage);
   1463                 } else {
   1464                     showToast(R.string.contactsJoinedNamedMessage, name);
   1465                 }
   1466             }
   1467             LocalBroadcastManager.getInstance(this)
   1468                     .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
   1469         } else {
   1470             if (receiver != null) {
   1471                 receiver.send(CP2_ERROR, new Bundle());
   1472             }
   1473             showToast(R.string.contactJoinErrorToast);
   1474         }
   1475     }
   1476 
   1477     /** Get the display name of the top-level contact after the contacts have been linked. */
   1478     private String queryNameOfLinkedContacts(long[] contactIds) {
   1479         final StringBuilder whereBuilder = new StringBuilder(Contacts._ID).append(" IN (");
   1480         final String[] whereArgs = new String[contactIds.length];
   1481         for (int i = 0; i < contactIds.length; i++) {
   1482             whereArgs[i] = String.valueOf(contactIds[i]);
   1483             whereBuilder.append("?,");
   1484         }
   1485         whereBuilder.deleteCharAt(whereBuilder.length() - 1).append(')');
   1486         final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
   1487                 new String[]{Contacts._ID, Contacts.DISPLAY_NAME,
   1488                         Contacts.DISPLAY_NAME_ALTERNATIVE},
   1489                 whereBuilder.toString(), whereArgs, null);
   1490 
   1491         String name = null;
   1492         String nameAlt = null;
   1493         long contactId = 0;
   1494         try {
   1495             if (cursor.moveToFirst()) {
   1496                 contactId = cursor.getLong(0);
   1497                 name = cursor.getString(1);
   1498                 nameAlt = cursor.getString(2);
   1499             }
   1500             while(cursor.moveToNext()) {
   1501                 if (cursor.getLong(0) != contactId) {
   1502                     return null;
   1503                 }
   1504             }
   1505 
   1506             final String formattedName = ContactDisplayUtils.getPreferredDisplayName(name, nameAlt,
   1507                     new ContactsPreferences(getApplicationContext()));
   1508             return formattedName == null ? "" : formattedName;
   1509         } finally {
   1510             if (cursor != null) {
   1511                 cursor.close();
   1512             }
   1513         }
   1514     }
   1515 
   1516     /** Returns true if the batch was successfully applied and false otherwise. */
   1517     private boolean applyOperations(ContentResolver resolver,
   1518             ArrayList<ContentProviderOperation> operations) {
   1519         try {
   1520             final ContentProviderResult[] result =
   1521                     resolver.applyBatch(ContactsContract.AUTHORITY, operations);
   1522             for (int i = 0; i < result.length; ++i) {
   1523                 // if no rows were modified in the operation then we count it as fail.
   1524                 if (result[i].count < 0) {
   1525                     throw new OperationApplicationException();
   1526                 }
   1527             }
   1528             return true;
   1529         } catch (RemoteException | OperationApplicationException e) {
   1530             FeedbackHelper.sendFeedback(this, TAG,
   1531                     "Failed to apply aggregation exception batch", e);
   1532             showToast(R.string.contactSavedErrorToast);
   1533             return false;
   1534         }
   1535     }
   1536 
   1537     private void joinContacts(Intent intent) {
   1538         long contactId1 = intent.getLongExtra(EXTRA_CONTACT_ID1, -1);
   1539         long contactId2 = intent.getLongExtra(EXTRA_CONTACT_ID2, -1);
   1540 
   1541         // Load raw contact IDs for all raw contacts involved - currently edited and selected
   1542         // in the join UIs.
   1543         long rawContactIds[] = getRawContactIdsForAggregation(contactId1, contactId2);
   1544         if (rawContactIds == null) {
   1545             Log.e(TAG, "Invalid arguments for joinContacts request");
   1546             return;
   1547         }
   1548 
   1549         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
   1550 
   1551         // For each pair of raw contacts, insert an aggregation exception
   1552         for (int i = 0; i < rawContactIds.length; i++) {
   1553             for (int j = 0; j < rawContactIds.length; j++) {
   1554                 if (i != j) {
   1555                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
   1556                 }
   1557             }
   1558         }
   1559 
   1560         final ContentResolver resolver = getContentResolver();
   1561 
   1562         // Use the name for contactId1 as the name for the newly aggregated contact.
   1563         final Uri contactId1Uri = ContentUris.withAppendedId(
   1564                 Contacts.CONTENT_URI, contactId1);
   1565         final Uri entityUri = Uri.withAppendedPath(
   1566                 contactId1Uri, Contacts.Entity.CONTENT_DIRECTORY);
   1567         Cursor c = resolver.query(entityUri,
   1568                 ContactEntityQuery.PROJECTION, ContactEntityQuery.SELECTION, null, null);
   1569         if (c == null) {
   1570             Log.e(TAG, "Unable to open Contacts DB cursor");
   1571             showToast(R.string.contactSavedErrorToast);
   1572             return;
   1573         }
   1574         long dataIdToAddSuperPrimary = -1;
   1575         try {
   1576             if (c.moveToFirst()) {
   1577                 dataIdToAddSuperPrimary = c.getLong(ContactEntityQuery.DATA_ID);
   1578             }
   1579         } finally {
   1580             c.close();
   1581         }
   1582 
   1583         // Mark the name from contactId1 IS_SUPER_PRIMARY to make sure that the contact
   1584         // display name does not change as a result of the join.
   1585         if (dataIdToAddSuperPrimary != -1) {
   1586             Builder builder = ContentProviderOperation.newUpdate(
   1587                     ContentUris.withAppendedId(Data.CONTENT_URI, dataIdToAddSuperPrimary));
   1588             builder.withValue(Data.IS_SUPER_PRIMARY, 1);
   1589             builder.withValue(Data.IS_PRIMARY, 1);
   1590             operations.add(builder.build());
   1591         }
   1592 
   1593         // Apply all aggregation exceptions as one batch
   1594         final boolean success = applyOperations(resolver, operations);
   1595 
   1596         final String name = queryNameOfLinkedContacts(new long[] {contactId1, contactId2});
   1597         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
   1598         if (success && name != null) {
   1599             if (TextUtils.isEmpty(name)) {
   1600                 showToast(R.string.contactsJoinedMessage);
   1601             } else {
   1602                 showToast(R.string.contactsJoinedNamedMessage, name);
   1603             }
   1604             Uri uri = RawContacts.getContactLookupUri(resolver,
   1605                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
   1606             callbackIntent.setData(uri);
   1607             LocalBroadcastManager.getInstance(this)
   1608                     .sendBroadcast(new Intent(BROADCAST_LINK_COMPLETE));
   1609         }
   1610         deliverCallback(callbackIntent);
   1611     }
   1612 
   1613     /**
   1614      * Gets the raw contact ids for each contact id in {@param contactIds}. Each index of the outer
   1615      * array of the return value holds an array of raw contact ids for one contactId.
   1616      * @param contactIds
   1617      * @return
   1618      */
   1619     private long[][] getSeparatedRawContactIds(long[] contactIds) {
   1620         final long[][] rawContactIds = new long[contactIds.length][];
   1621         for (int i = 0; i < contactIds.length; i++) {
   1622             rawContactIds[i] = getRawContactIds(contactIds[i]);
   1623         }
   1624         return rawContactIds;
   1625     }
   1626 
   1627     /**
   1628      * Gets the raw contact ids associated with {@param contactId}.
   1629      * @param contactId
   1630      * @return Array of raw contact ids.
   1631      */
   1632     private long[] getRawContactIds(long contactId) {
   1633         final ContentResolver resolver = getContentResolver();
   1634         long rawContactIds[];
   1635 
   1636         final StringBuilder queryBuilder = new StringBuilder();
   1637             queryBuilder.append(RawContacts.CONTACT_ID)
   1638                     .append("=")
   1639                     .append(String.valueOf(contactId));
   1640 
   1641         final Cursor c = resolver.query(RawContacts.CONTENT_URI,
   1642                 JoinContactQuery.PROJECTION,
   1643                 queryBuilder.toString(),
   1644                 null, null);
   1645         if (c == null) {
   1646             Log.e(TAG, "Unable to open Contacts DB cursor");
   1647             return null;
   1648         }
   1649         try {
   1650             rawContactIds = new long[c.getCount()];
   1651             for (int i = 0; i < rawContactIds.length; i++) {
   1652                 c.moveToPosition(i);
   1653                 final long rawContactId = c.getLong(JoinContactQuery._ID);
   1654                 rawContactIds[i] = rawContactId;
   1655             }
   1656         } finally {
   1657             c.close();
   1658         }
   1659         return rawContactIds;
   1660     }
   1661 
   1662     private long[] getRawContactIdsForAggregation(long[] contactIds) {
   1663         if (contactIds == null) {
   1664             return null;
   1665         }
   1666 
   1667         final ContentResolver resolver = getContentResolver();
   1668 
   1669         final StringBuilder queryBuilder = new StringBuilder();
   1670         final String stringContactIds[] = new String[contactIds.length];
   1671         for (int i = 0; i < contactIds.length; i++) {
   1672             queryBuilder.append(RawContacts.CONTACT_ID + "=?");
   1673             stringContactIds[i] = String.valueOf(contactIds[i]);
   1674             if (contactIds[i] == -1) {
   1675                 return null;
   1676             }
   1677             if (i == contactIds.length -1) {
   1678                 break;
   1679             }
   1680             queryBuilder.append(" OR ");
   1681         }
   1682 
   1683         final Cursor c = resolver.query(RawContacts.CONTENT_URI,
   1684                 JoinContactQuery.PROJECTION,
   1685                 queryBuilder.toString(),
   1686                 stringContactIds, null);
   1687         if (c == null) {
   1688             Log.e(TAG, "Unable to open Contacts DB cursor");
   1689             showToast(R.string.contactSavedErrorToast);
   1690             return null;
   1691         }
   1692         long rawContactIds[];
   1693         try {
   1694             if (c.getCount() < 2) {
   1695                 Log.e(TAG, "Not enough raw contacts to aggregate together.");
   1696                 return null;
   1697             }
   1698             rawContactIds = new long[c.getCount()];
   1699             for (int i = 0; i < rawContactIds.length; i++) {
   1700                 c.moveToPosition(i);
   1701                 long rawContactId = c.getLong(JoinContactQuery._ID);
   1702                 rawContactIds[i] = rawContactId;
   1703             }
   1704         } finally {
   1705             c.close();
   1706         }
   1707         return rawContactIds;
   1708     }
   1709 
   1710     private long[] getRawContactIdsForAggregation(long contactId1, long contactId2) {
   1711         return getRawContactIdsForAggregation(new long[] {contactId1, contactId2});
   1712     }
   1713 
   1714     /**
   1715      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
   1716      */
   1717     private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
   1718             long rawContactId1, long rawContactId2) {
   1719         Builder builder =
   1720                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
   1721         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
   1722         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   1723         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   1724         operations.add(builder.build());
   1725     }
   1726 
   1727     /**
   1728      * Construct a {@link AggregationExceptions#TYPE_AUTOMATIC} or a
   1729      * {@link AggregationExceptions#TYPE_KEEP_SEPARATE} ContentProviderOperation if a hard split is
   1730      * requested.
   1731      */
   1732     private void buildSplitContactDiff(ArrayList<ContentProviderOperation> operations,
   1733             long rawContactId1, long rawContactId2, boolean hardSplit) {
   1734         final Builder builder =
   1735                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
   1736         builder.withValue(AggregationExceptions.TYPE,
   1737                 hardSplit
   1738                         ? AggregationExceptions.TYPE_KEEP_SEPARATE
   1739                         : AggregationExceptions.TYPE_AUTOMATIC);
   1740         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   1741         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   1742         operations.add(builder.build());
   1743     }
   1744 
   1745     /**
   1746      * Returns an intent that can start this service and cause it to sleep for the specified time.
   1747      *
   1748      * This exists purely for debugging and manual testing. Since this service uses a single thread
   1749      * it is useful to have a way to test behavior when work is queued up and most of the other
   1750      * operations complete too quickly to simulate that under normal conditions.
   1751      */
   1752     public static Intent createSleepIntent(Context context, long millis) {
   1753         return new Intent(context, ContactSaveService.class).setAction(ACTION_SLEEP)
   1754                 .putExtra(EXTRA_SLEEP_DURATION, millis);
   1755     }
   1756 
   1757     private void sleepForDebugging(Intent intent) {
   1758         long duration = intent.getLongExtra(EXTRA_SLEEP_DURATION, 1000);
   1759         if (Log.isLoggable(TAG, Log.DEBUG)) {
   1760             Log.d(TAG, "sleeping for " + duration + "ms");
   1761         }
   1762         try {
   1763             Thread.sleep(duration);
   1764         } catch (InterruptedException e) {
   1765             e.printStackTrace();
   1766         }
   1767         if (Log.isLoggable(TAG, Log.DEBUG)) {
   1768             Log.d(TAG, "finished sleeping");
   1769         }
   1770     }
   1771 
   1772     /**
   1773      * Shows a toast on the UI thread by formatting messageId using args.
   1774      * @param messageId id of message string
   1775      * @param args args to format string
   1776      */
   1777     private void showToast(final int messageId, final Object... args) {
   1778         final String message = getResources().getString(messageId, args);
   1779         mMainHandler.post(new Runnable() {
   1780             @Override
   1781             public void run() {
   1782                 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
   1783             }
   1784         });
   1785     }
   1786 
   1787 
   1788     /**
   1789      * Shows a toast on the UI thread.
   1790      */
   1791     private void showToast(final int message) {
   1792         mMainHandler.post(new Runnable() {
   1793 
   1794             @Override
   1795             public void run() {
   1796                 Toast.makeText(ContactSaveService.this, message, Toast.LENGTH_LONG).show();
   1797             }
   1798         });
   1799     }
   1800 
   1801     private void deliverCallback(final Intent callbackIntent) {
   1802         mMainHandler.post(new Runnable() {
   1803 
   1804             @Override
   1805             public void run() {
   1806                 deliverCallbackOnUiThread(callbackIntent);
   1807             }
   1808         });
   1809     }
   1810 
   1811     void deliverCallbackOnUiThread(final Intent callbackIntent) {
   1812         // TODO: this assumes that if there are multiple instances of the same
   1813         // activity registered, the last one registered is the one waiting for
   1814         // the callback. Validity of this assumption needs to be verified.
   1815         for (Listener listener : sListeners) {
   1816             if (callbackIntent.getComponent().equals(
   1817                     ((Activity) listener).getIntent().getComponent())) {
   1818                 listener.onServiceCompleted(callbackIntent);
   1819                 return;
   1820             }
   1821         }
   1822     }
   1823 
   1824     public interface GroupsDao {
   1825         Uri create(String title, AccountWithDataSet account);
   1826         int delete(Uri groupUri);
   1827         Bundle captureDeletionUndoData(Uri groupUri);
   1828         Uri undoDeletion(Bundle undoData);
   1829     }
   1830 
   1831     public static class GroupsDaoImpl implements GroupsDao {
   1832         public static final String KEY_GROUP_DATA = "groupData";
   1833         public static final String KEY_GROUP_MEMBERS = "groupMemberIds";
   1834 
   1835         private static final String TAG = "GroupsDao";
   1836         private final Context context;
   1837         private final ContentResolver contentResolver;
   1838 
   1839         public GroupsDaoImpl(Context context) {
   1840             this(context, context.getContentResolver());
   1841         }
   1842 
   1843         public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
   1844             this.context = context;
   1845             this.contentResolver = contentResolver;
   1846         }
   1847 
   1848         public Bundle captureDeletionUndoData(Uri groupUri) {
   1849             final long groupId = ContentUris.parseId(groupUri);
   1850             final Bundle result = new Bundle();
   1851 
   1852             final Cursor cursor = contentResolver.query(groupUri,
   1853                     new String[]{
   1854                             Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
   1855                             Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
   1856                             Groups.SHOULD_SYNC
   1857                     },
   1858                     Groups.DELETED + "=?", new String[] { "0" }, null);
   1859             try {
   1860                 if (cursor.moveToFirst()) {
   1861                     final ContentValues groupValues = new ContentValues();
   1862                     DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
   1863                     result.putParcelable(KEY_GROUP_DATA, groupValues);
   1864                 } else {
   1865                     // Group doesn't exist.
   1866                     return result;
   1867                 }
   1868             } finally {
   1869                 cursor.close();
   1870             }
   1871 
   1872             final Cursor membersCursor = contentResolver.query(
   1873                     Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
   1874                     Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
   1875                     new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
   1876             final long[] memberIds = new long[membersCursor.getCount()];
   1877             int i = 0;
   1878             while (membersCursor.moveToNext()) {
   1879                 memberIds[i++] = membersCursor.getLong(0);
   1880             }
   1881             result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
   1882             return result;
   1883         }
   1884 
   1885         public Uri undoDeletion(Bundle deletedGroupData) {
   1886             final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
   1887             if (groupData == null) {
   1888                 return null;
   1889             }
   1890             final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
   1891             final long groupId = ContentUris.parseId(groupUri);
   1892 
   1893             final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
   1894             if (memberIds == null) {
   1895                 return groupUri;
   1896             }
   1897             final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
   1898             for (int i = 0; i < memberIds.length; i++) {
   1899                 memberInsertions[i] = new ContentValues();
   1900                 memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
   1901                 memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
   1902                 memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
   1903             }
   1904             final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
   1905             if (inserted != memberIds.length) {
   1906                 Log.e(TAG, "Could not recover some members for group deletion undo");
   1907             }
   1908 
   1909             return groupUri;
   1910         }
   1911 
   1912         public Uri create(String title, AccountWithDataSet account) {
   1913             final ContentValues values = new ContentValues();
   1914             values.put(Groups.TITLE, title);
   1915             values.put(Groups.ACCOUNT_NAME, account.name);
   1916             values.put(Groups.ACCOUNT_TYPE, account.type);
   1917             values.put(Groups.DATA_SET, account.dataSet);
   1918             return contentResolver.insert(Groups.CONTENT_URI, values);
   1919         }
   1920 
   1921         public int delete(Uri groupUri) {
   1922             return contentResolver.delete(groupUri, null, null);
   1923         }
   1924     }
   1925 
   1926     /**
   1927      * Keeps track of which operations have been requested but have not yet finished for this
   1928      * service.
   1929      */
   1930     public static class State {
   1931         private final CopyOnWriteArrayList<Intent> mPending;
   1932 
   1933         public State() {
   1934             mPending = new CopyOnWriteArrayList<>();
   1935         }
   1936 
   1937         public State(Collection<Intent> pendingActions) {
   1938             mPending = new CopyOnWriteArrayList<>(pendingActions);
   1939         }
   1940 
   1941         public boolean isIdle() {
   1942             return mPending.isEmpty();
   1943         }
   1944 
   1945         public Intent getCurrentIntent() {
   1946             return mPending.isEmpty() ? null : mPending.get(0);
   1947         }
   1948 
   1949         /**
   1950          * Returns the first intent requested that has the specified action or null if no intent
   1951          * with that action has been requested.
   1952          */
   1953         public Intent getNextIntentWithAction(String action) {
   1954             for (Intent intent : mPending) {
   1955                 if (action.equals(intent.getAction())) {
   1956                     return intent;
   1957                 }
   1958             }
   1959             return null;
   1960         }
   1961 
   1962         public boolean isActionPending(String action) {
   1963             return getNextIntentWithAction(action) != null;
   1964         }
   1965 
   1966         private void onFinish(Intent intent) {
   1967             if (mPending.isEmpty()) {
   1968                 return;
   1969             }
   1970             final String action = mPending.get(0).getAction();
   1971             if (action.equals(intent.getAction())) {
   1972                 mPending.remove(0);
   1973             }
   1974         }
   1975 
   1976         private void onStart(Intent intent) {
   1977             if (intent.getAction() == null) {
   1978                 return;
   1979             }
   1980             mPending.add(intent);
   1981         }
   1982     }
   1983 }
   1984