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