1 /*
2  * Copyright (C) 2009 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.model;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.ContentProviderResult;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.provider.BaseColumns;
26 import android.provider.ContactsContract.AggregationExceptions;
27 import android.provider.ContactsContract.CommonDataKinds.Email;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.Data;
30 import android.provider.ContactsContract.RawContacts;
31 import android.test.AndroidTestCase;
32 
33 import androidx.test.filters.LargeTest;
34 
35 import com.android.contacts.compat.CompatUtils;
36 import com.android.contacts.model.account.AccountType;
37 
38 import com.google.common.collect.Lists;
39 
40 import java.lang.reflect.Field;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 
44 /**
45  * Tests for {@link RawContactDeltaList} which focus on "diff" operations that should
46  * create {@link AggregationExceptions} in certain cases.
47  */
48 @LargeTest
49 public class RawContactDeltaListTests extends AndroidTestCase {
50     // From android.content.ContentProviderOperation
51     public static final int TYPE_INSERT = 1;
52     public static final int TYPE_UPDATE = 2;
53     public static final int TYPE_DELETE = 3;
54     public static final int TYPE_ASSERT = 4;
55 
56     private static final long CONTACT_FIRST = 1;
57     private static final long CONTACT_SECOND = 2;
58 
59     public static final long CONTACT_BOB = 10;
60     public static final long CONTACT_MARY = 11;
61 
62     public static final long INSERTED_CONTACT_ID = 3;
63 
64     public static final long PHONE_RED = 20;
65     public static final long PHONE_GREEN = 21;
66     public static final long PHONE_BLUE = 22;
67 
68     public static final long EMAIL_YELLOW = 25;
69 
70     public static final long VER_FIRST = 100;
71     public static final long VER_SECOND = 200;
72 
73     public static final String TEST_PHONE = "555-1212";
74     public static final String TEST_ACCOUNT = "org.example.test";
75 
RawContactDeltaListTests()76     public RawContactDeltaListTests() {
77         super();
78     }
79 
80     @Override
setUp()81     public void setUp() {
82         mContext = getContext();
83     }
84 
85     /**
86      * Build a {@link AccountType} that has various odd constraints for
87      * testing purposes.
88      */
getAccountType()89     protected AccountType getAccountType() {
90         return new RawContactModifierTests.MockContactsSource();
91     }
92 
getValues(ContentProviderOperation operation)93     static ContentValues getValues(ContentProviderOperation operation)
94             throws NoSuchFieldException, IllegalAccessException {
95         final Field field = ContentProviderOperation.class.getDeclaredField("mValues");
96         field.setAccessible(true);
97         return (ContentValues) field.get(operation);
98     }
99 
getUpdate(Context context, long rawContactId)100     static RawContactDelta getUpdate(Context context, long rawContactId) {
101         final RawContact before = RawContactDeltaTests.getRawContact(context, rawContactId,
102                 RawContactDeltaTests.TEST_PHONE_ID);
103         return RawContactDelta.fromBefore(before);
104     }
105 
getInsert()106     static RawContactDelta getInsert() {
107         final ContentValues after = new ContentValues();
108         after.put(RawContacts.ACCOUNT_NAME, RawContactDeltaTests.TEST_ACCOUNT_NAME);
109         after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
110 
111         final ValuesDelta values = ValuesDelta.fromAfter(after);
112         return new RawContactDelta(values);
113     }
114 
buildSet(RawContactDelta... deltas)115     static RawContactDeltaList buildSet(RawContactDelta... deltas) {
116         final RawContactDeltaList set = new RawContactDeltaList();
117         Collections.addAll(set, deltas);
118         return set;
119     }
120 
buildBeforeEntity(Context context, long rawContactId, long version, ContentValues... entries)121     static RawContactDelta buildBeforeEntity(Context context, long rawContactId, long version,
122             ContentValues... entries) {
123         // Build an existing contact read from database
124         final ContentValues contact = new ContentValues();
125         contact.put(RawContacts.VERSION, version);
126         contact.put(RawContacts._ID, rawContactId);
127         final RawContact before = new RawContact(contact);
128         for (ContentValues entry : entries) {
129             before.addDataItemValues(entry);
130         }
131         return RawContactDelta.fromBefore(before);
132     }
133 
buildAfterEntity(ContentValues... entries)134     static RawContactDelta buildAfterEntity(ContentValues... entries) {
135         // Build an existing contact read from database
136         final ContentValues contact = new ContentValues();
137         contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT);
138         final RawContactDelta after = new RawContactDelta(ValuesDelta.fromAfter(contact));
139         for (ContentValues entry : entries) {
140             after.addEntry(ValuesDelta.fromAfter(entry));
141         }
142         return after;
143     }
144 
buildPhone(long phoneId)145     static ContentValues buildPhone(long phoneId) {
146         return buildPhone(phoneId, Long.toString(phoneId));
147     }
148 
buildPhone(long phoneId, String value)149     static ContentValues buildPhone(long phoneId, String value) {
150         final ContentValues values = new ContentValues();
151         values.put(Data._ID, phoneId);
152         values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
153         values.put(Phone.NUMBER, value);
154         values.put(Phone.TYPE, Phone.TYPE_HOME);
155         return values;
156     }
157 
buildEmail(long emailId)158     static ContentValues buildEmail(long emailId) {
159         final ContentValues values = new ContentValues();
160         values.put(Data._ID, emailId);
161         values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
162         values.put(Email.DATA, Long.toString(emailId));
163         values.put(Email.TYPE, Email.TYPE_HOME);
164         return values;
165     }
166 
insertPhone(RawContactDeltaList set, long rawContactId, ContentValues values)167     static void insertPhone(RawContactDeltaList set, long rawContactId, ContentValues values) {
168         final RawContactDelta match = set.getByRawContactId(rawContactId);
169         match.addEntry(ValuesDelta.fromAfter(values));
170     }
171 
getPhone(RawContactDeltaList set, long rawContactId, long dataId)172     static ValuesDelta getPhone(RawContactDeltaList set, long rawContactId, long dataId) {
173         final RawContactDelta match = set.getByRawContactId(rawContactId);
174         return match.getEntry(dataId);
175     }
176 
assertDiffPattern(RawContactDelta delta, CPOWrapper... pattern)177     static void assertDiffPattern(RawContactDelta delta, CPOWrapper... pattern) {
178         final ArrayList<CPOWrapper> diff = Lists.newArrayList();
179         delta.buildAssertWrapper(diff);
180         delta.buildDiffWrapper(diff);
181         assertDiffPattern(diff, pattern);
182     }
183 
assertDiffPattern(RawContactDeltaList set, CPOWrapper... pattern)184     static void assertDiffPattern(RawContactDeltaList set, CPOWrapper... pattern) {
185         assertDiffPattern(set.buildDiffWrapper(), pattern);
186     }
187 
assertDiffPattern(ArrayList<CPOWrapper> diff, CPOWrapper... pattern)188     static void assertDiffPattern(ArrayList<CPOWrapper> diff, CPOWrapper... pattern) {
189         assertEquals("Unexpected operations", pattern.length, diff.size());
190 
191         ContentProviderResult[] fakeBackReferences = new ContentProviderResult[diff.size()];
192         for (int i = 0; i < pattern.length; i++) {
193             final CPOWrapper expected = pattern[i];
194             final CPOWrapper found = diff.get(i);
195 
196             assertEquals("Unexpected uri",
197                     expected.getOperation().getUri(), found.getOperation().getUri());
198 
199             final String expectedType = getTypeString(expected);
200             final String foundType = getTypeString(found);
201             assertEquals("Unexpected type", expectedType, foundType);
202 
203             if (CompatUtils.isDeleteCompat(expected)) {
204                 continue;
205             }
206 
207             if (CompatUtils.isInsertCompat(found)) {
208                 fakeBackReferences[i] = new ContentProviderResult(
209                         ContentUris.withAppendedId(RawContacts.CONTENT_URI, INSERTED_CONTACT_ID));
210             } else if (CompatUtils.isUpdateCompat(found)) {
211                 fakeBackReferences[i] = new ContentProviderResult(1);
212             }
213 
214 
215             ContentValues expectedValues = expected.getOperation().resolveValueBackReferences(
216                     new ContentProviderResult[0], 0);
217             ContentValues foundValues = found.getOperation().resolveValueBackReferences(
218                     fakeBackReferences, fakeBackReferences.length);
219             expectedValues.remove(BaseColumns._ID);
220             foundValues.remove(BaseColumns._ID);
221             assertEquals("Unexpected values", expectedValues, foundValues);
222         }
223     }
224 
getTypeString(CPOWrapper cpoWrapper)225     static String getTypeString(CPOWrapper cpoWrapper) {
226         if (CompatUtils.isAssertQueryCompat(cpoWrapper)) {
227             return "TYPE_ASSERT";
228         } else if (CompatUtils.isInsertCompat(cpoWrapper)) {
229             return "TYPE_INSERT";
230         } else if (CompatUtils.isUpdateCompat(cpoWrapper)) {
231             return "TYPE_UPDATE";
232         } else if (CompatUtils.isDeleteCompat(cpoWrapper)) {
233             return "TYPE_DELETE";
234         }
235         return "TYPE_UNKNOWN";
236     }
237 
buildAssertVersion(long version)238     static CPOWrapper buildAssertVersion(long version) {
239         final ContentValues values = new ContentValues();
240         values.put(RawContacts.VERSION, version);
241         return buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_ASSERT, values);
242     }
243 
buildAggregationModeUpdate(int mode)244     static CPOWrapper buildAggregationModeUpdate(int mode) {
245         final ContentValues values = new ContentValues();
246         values.put(RawContacts.AGGREGATION_MODE, mode);
247         return buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_UPDATE, values);
248     }
249 
buildUpdateAggregationSuspended()250     static CPOWrapper buildUpdateAggregationSuspended() {
251         return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_SUSPENDED);
252     }
253 
buildUpdateAggregationDefault()254     static CPOWrapper buildUpdateAggregationDefault() {
255         return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT);
256     }
257 
buildUpdateAggregationKeepTogether(long rawContactId)258     static CPOWrapper buildUpdateAggregationKeepTogether(long rawContactId) {
259         final ContentValues values = new ContentValues();
260         values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
261         values.put(AggregationExceptions.RAW_CONTACT_ID2, INSERTED_CONTACT_ID);
262         values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
263         return buildCPOWrapper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE, values);
264     }
265 
buildDataInsert(ValuesDelta values, long rawContactId)266     static ContentValues buildDataInsert(ValuesDelta values, long rawContactId) {
267         final ContentValues insertValues = values.getCompleteValues();
268         insertValues.put(Data.RAW_CONTACT_ID, rawContactId);
269         return insertValues;
270     }
271 
buildDelete(Uri uri)272     static CPOWrapper buildDelete(Uri uri) {
273         return buildCPOWrapper(uri, TYPE_DELETE, (ContentValues) null);
274     }
275 
buildOper(Uri uri, int type, ValuesDelta values)276     static ContentProviderOperation buildOper(Uri uri, int type, ValuesDelta values) {
277         return buildOper(uri, type, values.getCompleteValues());
278     }
279 
buildOper(Uri uri, int type, ContentValues values)280     static ContentProviderOperation buildOper(Uri uri, int type, ContentValues values) {
281         switch (type) {
282             case TYPE_ASSERT:
283                 return ContentProviderOperation.newAssertQuery(uri).withValues(values).build();
284             case TYPE_INSERT:
285                 return ContentProviderOperation.newInsert(uri).withValues(values).build();
286             case TYPE_UPDATE:
287                 return ContentProviderOperation.newUpdate(uri).withValues(values).build();
288             case TYPE_DELETE:
289                 return ContentProviderOperation.newDelete(uri).build();
290         }
291         return null;
292     }
293 
buildCPOWrapper(Uri uri, int type, ContentValues values)294     static CPOWrapper buildCPOWrapper(Uri uri, int type, ContentValues values) {
295         if (type == TYPE_ASSERT || type == TYPE_INSERT || type == TYPE_UPDATE
296                 || type == TYPE_DELETE) {
297             return new CPOWrapper(buildOper(uri, type, values), type);
298         }
299         return null;
300     }
301 
getVersion(RawContactDeltaList set, Long rawContactId)302     static Long getVersion(RawContactDeltaList set, Long rawContactId) {
303         return set.getByRawContactId(rawContactId).getValues().getAsLong(RawContacts.VERSION);
304     }
305 
306     /**
307      * Count number of {@link AggregationExceptions} updates contained in the
308      * given list of {@link CPOWrapper}.
309      */
countExceptionUpdates(ArrayList<CPOWrapper> diff)310     static int countExceptionUpdates(ArrayList<CPOWrapper> diff) {
311         int updateCount = 0;
312         for (CPOWrapper cpoWrapper : diff) {
313             final ContentProviderOperation oper = cpoWrapper.getOperation();
314             if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
315                     && CompatUtils.isUpdateCompat(cpoWrapper)) {
316                 updateCount++;
317             }
318         }
319         return updateCount;
320     }
321 
testInsert()322     public void testInsert() {
323         final RawContactDelta insert = getInsert();
324         final RawContactDeltaList set = buildSet(insert);
325 
326         // Inserting single shouldn't create rules
327         final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
328         final int exceptionCount = countExceptionUpdates(diff);
329         assertEquals("Unexpected exception updates", 0, exceptionCount);
330     }
331 
testUpdateUpdate()332     public void testUpdateUpdate() {
333         final RawContactDelta updateFirst = getUpdate(mContext, CONTACT_FIRST);
334         final RawContactDelta updateSecond = getUpdate(mContext, CONTACT_SECOND);
335         final RawContactDeltaList set = buildSet(updateFirst, updateSecond);
336 
337         // Updating two existing shouldn't create rules
338         final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
339         final int exceptionCount = countExceptionUpdates(diff);
340         assertEquals("Unexpected exception updates", 0, exceptionCount);
341     }
342 
testUpdateInsert()343     public void testUpdateInsert() {
344         final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
345         final RawContactDelta insert = getInsert();
346         final RawContactDeltaList set = buildSet(update, insert);
347 
348         // New insert should only create one rule
349         final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
350         final int exceptionCount = countExceptionUpdates(diff);
351         assertEquals("Unexpected exception updates", 1, exceptionCount);
352     }
353 
testInsertUpdateInsert()354     public void testInsertUpdateInsert() {
355         final RawContactDelta insertFirst = getInsert();
356         final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
357         final RawContactDelta insertSecond = getInsert();
358         final RawContactDeltaList set = buildSet(insertFirst, update, insertSecond);
359 
360         // Two inserts should create two rules to bind against single existing
361         final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
362         final int exceptionCount = countExceptionUpdates(diff);
363         assertEquals("Unexpected exception updates", 2, exceptionCount);
364     }
365 
testInsertInsertInsert()366     public void testInsertInsertInsert() {
367         final RawContactDelta insertFirst = getInsert();
368         final RawContactDelta insertSecond = getInsert();
369         final RawContactDelta insertThird = getInsert();
370         final RawContactDeltaList set = buildSet(insertFirst, insertSecond, insertThird);
371 
372         // Three new inserts should create only two binding rules
373         final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
374         final int exceptionCount = countExceptionUpdates(diff);
375         assertEquals("Unexpected exception updates", 2, exceptionCount);
376     }
377 
testMergeDataRemoteInsert()378     public void testMergeDataRemoteInsert() {
379         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
380                 VER_FIRST, buildPhone(PHONE_RED)));
381         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
382                 VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
383 
384         // Merge in second version, verify they match
385         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
386         assertEquals("Unexpected change when merging", second, merged);
387     }
388 
testMergeDataLocalUpdateRemoteInsert()389     public void testMergeDataLocalUpdateRemoteInsert() {
390         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
391                 VER_FIRST, buildPhone(PHONE_RED)));
392         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
393                 VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
394 
395         // Change the local number to trigger update
396         final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
397         phone.put(Phone.NUMBER, TEST_PHONE);
398 
399         assertDiffPattern(first,
400                 buildAssertVersion(VER_FIRST),
401                 buildUpdateAggregationSuspended(),
402                 buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
403                 buildUpdateAggregationDefault());
404 
405         // Merge in the second version, verify diff matches
406         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
407         assertDiffPattern(merged,
408                 buildAssertVersion(VER_SECOND),
409                 buildUpdateAggregationSuspended(),
410                 buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
411                 buildUpdateAggregationDefault());
412     }
413 
testMergeDataLocalUpdateRemoteDelete()414     public void testMergeDataLocalUpdateRemoteDelete() {
415         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
416                 VER_FIRST, buildPhone(PHONE_RED)));
417         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
418                 VER_SECOND, buildPhone(PHONE_GREEN)));
419 
420         // Change the local number to trigger update
421         final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
422         phone.put(Phone.NUMBER, TEST_PHONE);
423 
424         assertDiffPattern(first,
425                 buildAssertVersion(VER_FIRST),
426                 buildUpdateAggregationSuspended(),
427                 buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
428                 buildUpdateAggregationDefault());
429 
430         // Merge in the second version, verify that our update changed to
431         // insert, since RED was deleted on remote side
432         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
433         assertDiffPattern(merged,
434                 buildAssertVersion(VER_SECOND),
435                 buildUpdateAggregationSuspended(),
436                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(phone, CONTACT_BOB)),
437                 buildUpdateAggregationDefault());
438     }
439 
testMergeDataLocalDeleteRemoteUpdate()440     public void testMergeDataLocalDeleteRemoteUpdate() {
441         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
442                 VER_FIRST, buildPhone(PHONE_RED)));
443         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
444                 VER_SECOND, buildPhone(PHONE_RED, TEST_PHONE)));
445 
446         // Delete phone locally
447         final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
448         phone.markDeleted();
449 
450         assertDiffPattern(first,
451                 buildAssertVersion(VER_FIRST),
452                 buildUpdateAggregationSuspended(),
453                 buildDelete(Data.CONTENT_URI),
454                 buildUpdateAggregationDefault());
455 
456         // Merge in the second version, verify that our delete remains
457         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
458         assertDiffPattern(merged,
459                 buildAssertVersion(VER_SECOND),
460                 buildUpdateAggregationSuspended(),
461                 buildDelete(Data.CONTENT_URI),
462                 buildUpdateAggregationDefault());
463     }
464 
testMergeDataLocalInsertRemoteInsert()465     public void testMergeDataLocalInsertRemoteInsert() {
466         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
467                 VER_FIRST, buildPhone(PHONE_RED)));
468         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
469                 VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
470 
471         // Insert new phone locally
472         final ValuesDelta bluePhone = ValuesDelta.fromAfter(buildPhone(PHONE_BLUE));
473         first.getByRawContactId(CONTACT_BOB).addEntry(bluePhone);
474         assertDiffPattern(first,
475                 buildAssertVersion(VER_FIRST),
476                 buildUpdateAggregationSuspended(),
477                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
478                         buildDataInsert(bluePhone, CONTACT_BOB)),
479                 buildUpdateAggregationDefault());
480 
481         // Merge in the second version, verify that our insert remains
482         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
483         assertDiffPattern(merged,
484                 buildAssertVersion(VER_SECOND),
485                 buildUpdateAggregationSuspended(),
486                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
487                         buildDataInsert(bluePhone, CONTACT_BOB)),
488                 buildUpdateAggregationDefault());
489     }
490 
testMergeRawContactLocalInsertRemoteInsert()491     public void testMergeRawContactLocalInsertRemoteInsert() {
492         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
493                 VER_FIRST, buildPhone(PHONE_RED)));
494         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
495                 VER_SECOND, buildPhone(PHONE_RED)), buildBeforeEntity(mContext, CONTACT_MARY,
496                 VER_SECOND, buildPhone(PHONE_RED)));
497 
498         // Add new contact locally, should remain insert
499         final ContentValues joePhoneInsert = buildPhone(PHONE_BLUE);
500         joePhoneInsert.put(Data.RAW_CONTACT_ID, INSERTED_CONTACT_ID);
501         final RawContactDelta joeContact = buildAfterEntity(joePhoneInsert);
502         final ContentValues joeContactInsert = joeContact.getValues().getCompleteValues();
503         joeContactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
504         first.add(joeContact);
505         assertDiffPattern(first,
506                 buildAssertVersion(VER_FIRST),
507                 buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
508                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
509                 buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
510                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
511 
512         // Merge in the second version, verify that our insert remains
513         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
514         assertDiffPattern(merged,
515                 buildAssertVersion(VER_SECOND),
516                 buildAssertVersion(VER_SECOND),
517                 buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
518                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
519                 buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
520                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
521     }
522 
testMergeRawContactLocalDeleteRemoteDelete()523     public void testMergeRawContactLocalDeleteRemoteDelete() {
524         final RawContactDeltaList first = buildSet(
525                 buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
526                 buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
527         final RawContactDeltaList second = buildSet(
528                 buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
529 
530         // Remove contact locally
531         first.getByRawContactId(CONTACT_MARY).markDeleted();
532         assertDiffPattern(first,
533                 buildAssertVersion(VER_FIRST),
534                 buildAssertVersion(VER_FIRST),
535                 buildDelete(RawContacts.CONTENT_URI));
536 
537         // Merge in the second version, verify that our delete isn't needed
538         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
539         assertDiffPattern(merged);
540     }
541 
testMergeRawContactLocalUpdateRemoteDelete()542     public void testMergeRawContactLocalUpdateRemoteDelete() {
543         final RawContactDeltaList first = buildSet(
544                 buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
545                 buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
546         final RawContactDeltaList second = buildSet(
547                 buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
548 
549         // Perform local update
550         final ValuesDelta phone = getPhone(first, CONTACT_MARY, PHONE_RED);
551         phone.put(Phone.NUMBER, TEST_PHONE);
552         assertDiffPattern(first,
553                 buildAssertVersion(VER_FIRST),
554                 buildAssertVersion(VER_FIRST),
555                 buildUpdateAggregationSuspended(),
556                 buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
557                 buildUpdateAggregationDefault());
558 
559         final ContentValues phoneInsert = phone.getCompleteValues();
560         phoneInsert.put(Data.RAW_CONTACT_ID, INSERTED_CONTACT_ID);
561         final ContentValues contactInsert = first.getByRawContactId(CONTACT_MARY).getValues()
562                 .getCompleteValues();
563         contactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
564 
565         // Merge and verify that update turned into insert
566         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
567         assertDiffPattern(merged,
568                 buildAssertVersion(VER_SECOND),
569                 buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, contactInsert),
570                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, phoneInsert),
571                 buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
572                 buildUpdateAggregationKeepTogether(CONTACT_BOB));
573     }
574 
testMergeUsesNewVersion()575     public void testMergeUsesNewVersion() {
576         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
577                 VER_FIRST, buildPhone(PHONE_RED)));
578         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
579                 VER_SECOND, buildPhone(PHONE_RED)));
580 
581         assertEquals((Long) VER_FIRST, getVersion(first, CONTACT_BOB));
582         assertEquals((Long) VER_SECOND, getVersion(second, CONTACT_BOB));
583 
584         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
585         assertEquals((Long) VER_SECOND, getVersion(merged, CONTACT_BOB));
586     }
587 
testMergeAfterEnsureAndTrim()588     public void testMergeAfterEnsureAndTrim() {
589         final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
590                 VER_FIRST, buildEmail(EMAIL_YELLOW)));
591         final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
592                 VER_SECOND, buildEmail(EMAIL_YELLOW)));
593 
594         // Ensure we have at least one phone
595         final AccountType source = getAccountType();
596         final RawContactDelta bobContact = first.getByRawContactId(CONTACT_BOB);
597         RawContactModifier.ensureKindExists(bobContact, source, Phone.CONTENT_ITEM_TYPE);
598         final ValuesDelta bobPhone = bobContact.getSuperPrimaryEntry(Phone.CONTENT_ITEM_TYPE, true);
599 
600         // Make sure the update would insert a row
601         assertDiffPattern(first,
602                 buildAssertVersion(VER_FIRST),
603                 buildUpdateAggregationSuspended(),
604                 buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
605                         buildDataInsert(bobPhone, CONTACT_BOB)),
606                 buildUpdateAggregationDefault());
607 
608         // Trim values and ensure that we don't insert things
609         RawContactModifier.trimEmpty(bobContact, source);
610         assertDiffPattern(first);
611 
612         // Now re-parent the change, which should remain no-op
613         final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
614         assertDiffPattern(merged);
615     }
616 }
617