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