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