Home | History | Annotate | Download | only in contacts
      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;
     18 
     19 import static android.content.ContentProviderOperation.TYPE_DELETE;
     20 import static android.content.ContentProviderOperation.TYPE_INSERT;
     21 import static android.content.ContentProviderOperation.TYPE_UPDATE;
     22 
     23 import com.android.contacts.model.ContactsSource;
     24 import com.android.contacts.model.EntityDelta;
     25 import com.android.contacts.model.EntityModifier;
     26 import com.android.contacts.model.EntitySet;
     27 import com.android.contacts.model.Sources;
     28 import com.android.contacts.model.ContactsSource.DataKind;
     29 import com.android.contacts.model.ContactsSource.EditType;
     30 import com.android.contacts.model.EntityDelta.ValuesDelta;
     31 import com.google.android.collect.Lists;
     32 
     33 import android.content.ContentProviderOperation;
     34 import android.content.ContentValues;
     35 import android.content.Context;
     36 import android.content.Entity;
     37 import android.os.Bundle;
     38 import android.provider.ContactsContract.Intents.Insert;
     39 import android.provider.ContactsContract.Data;
     40 import android.provider.ContactsContract.RawContacts;
     41 import android.provider.ContactsContract.CommonDataKinds.Email;
     42 import android.provider.ContactsContract.CommonDataKinds.Im;
     43 import android.provider.ContactsContract.CommonDataKinds.Organization;
     44 import android.provider.ContactsContract.CommonDataKinds.Phone;
     45 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     46 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     47 import android.test.AndroidTestCase;
     48 import android.test.suitebuilder.annotation.LargeTest;
     49 
     50 import java.util.ArrayList;
     51 import java.util.List;
     52 
     53 /**
     54  * Tests for {@link EntityModifier} to verify that {@link ContactsSource}
     55  * constraints are being enforced correctly.
     56  */
     57 @LargeTest
     58 public class EntityModifierTests extends AndroidTestCase {
     59     public static final String TAG = "EntityModifierTests";
     60 
     61     public static final long VER_FIRST = 100;
     62 
     63     private static final long TEST_ID = 4;
     64     private static final String TEST_PHONE = "218-555-1212";
     65     private static final String TEST_NAME = "Adam Young";
     66     private static final String TEST_NAME2 = "Breanne Duren";
     67     private static final String TEST_IM = "example (at) example.com";
     68     private static final String TEST_POSTAL = "1600 Amphitheatre Parkway";
     69 
     70     private static final String TEST_ACCOUNT_NAME = "unittest (at) example.com";
     71     private static final String TEST_ACCOUNT_TYPE = "com.example.unittest";
     72 
     73     public EntityModifierTests() {
     74         super();
     75     }
     76 
     77     @Override
     78     public void setUp() {
     79         mContext = getContext();
     80     }
     81 
     82     public static class MockContactsSource extends ContactsSource {
     83         @Override
     84         protected void inflate(Context context, int inflateLevel) {
     85             this.accountType = TEST_ACCOUNT_TYPE;
     86             this.setInflatedLevel(ContactsSource.LEVEL_CONSTRAINTS);
     87 
     88             // Phone allows maximum 2 home, 1 work, and unlimited other, with
     89             // constraint of 5 numbers maximum.
     90             DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE, -1, -1, 10, true);
     91 
     92             kind.typeOverallMax = 5;
     93             kind.typeColumn = Phone.TYPE;
     94             kind.typeList = Lists.newArrayList();
     95             kind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
     96             kind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
     97             kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
     98             kind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
     99 
    100             kind.fieldList = Lists.newArrayList();
    101             kind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
    102             kind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
    103 
    104             addKind(kind);
    105 
    106             // Email is unlimited
    107             kind = new DataKind(Email.CONTENT_ITEM_TYPE, -1, -1, 10, true);
    108             kind.typeOverallMax = -1;
    109             kind.fieldList = Lists.newArrayList();
    110             kind.fieldList.add(new EditField(Email.DATA, -1, -1));
    111             addKind(kind);
    112 
    113             // IM is only one
    114             kind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, -1, 10, true);
    115             kind.typeOverallMax = 1;
    116             kind.fieldList = Lists.newArrayList();
    117             kind.fieldList.add(new EditField(Im.DATA, -1, -1));
    118             addKind(kind);
    119 
    120             // Organization is only one
    121             kind = new DataKind(Organization.CONTENT_ITEM_TYPE, -1, -1, 10, true);
    122             kind.typeOverallMax = 1;
    123             kind.fieldList = Lists.newArrayList();
    124             kind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
    125             kind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
    126             addKind(kind);
    127         }
    128 
    129         @Override
    130         public int getHeaderColor(Context context) {
    131             return 0;
    132         }
    133 
    134         @Override
    135         public int getSideBarColor(Context context) {
    136             return 0xffffff;
    137         }
    138     }
    139 
    140     /**
    141      * Build a {@link ContactsSource} that has various odd constraints for
    142      * testing purposes.
    143      */
    144     protected ContactsSource getSource() {
    145         final ContactsSource source = new MockContactsSource();
    146         source.ensureInflated(getContext(), ContactsSource.LEVEL_CONSTRAINTS);
    147         return source;
    148     }
    149 
    150     /**
    151      * Build {@link Sources} instance.
    152      */
    153     protected Sources getSources(ContactsSource... sources) {
    154         return new Sources(sources);
    155     }
    156 
    157     /**
    158      * Build an {@link Entity} with the requested set of phone numbers.
    159      */
    160     protected EntityDelta getEntity(Long existingId, ContentValues... entries) {
    161         final ContentValues contact = new ContentValues();
    162         if (existingId != null) {
    163             contact.put(RawContacts._ID, existingId);
    164         }
    165         contact.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
    166         contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE);
    167 
    168         final Entity before = new Entity(contact);
    169         for (ContentValues values : entries) {
    170             before.addSubValue(Data.CONTENT_URI, values);
    171         }
    172         return EntityDelta.fromBefore(before);
    173     }
    174 
    175     /**
    176      * Assert this {@link List} contains the given {@link Object}.
    177      */
    178     protected void assertContains(List<?> list, Object object) {
    179         assertTrue("Missing expected value", list.contains(object));
    180     }
    181 
    182     /**
    183      * Assert this {@link List} does not contain the given {@link Object}.
    184      */
    185     protected void assertNotContains(List<?> list, Object object) {
    186         assertFalse("Contained unexpected value", list.contains(object));
    187     }
    188 
    189     /**
    190      * Insert various rows to test
    191      * {@link EntityModifier#getValidTypes(EntityDelta, DataKind, EditType)}
    192      */
    193     public void testValidTypes() {
    194         // Build a source and pull specific types
    195         final ContactsSource source = getSource();
    196         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    197         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    198         final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
    199         final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
    200 
    201         List<EditType> validTypes;
    202 
    203         // Add first home, first work
    204         final EntityDelta state = getEntity(TEST_ID);
    205         EntityModifier.insertChild(state, kindPhone, typeHome);
    206         EntityModifier.insertChild(state, kindPhone, typeWork);
    207 
    208         // Expecting home, other
    209         validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
    210         assertContains(validTypes, typeHome);
    211         assertNotContains(validTypes, typeWork);
    212         assertContains(validTypes, typeOther);
    213 
    214         // Add second home
    215         EntityModifier.insertChild(state, kindPhone, typeHome);
    216 
    217         // Expecting other
    218         validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
    219         assertNotContains(validTypes, typeHome);
    220         assertNotContains(validTypes, typeWork);
    221         assertContains(validTypes, typeOther);
    222 
    223         // Add third and fourth home (invalid, but possible)
    224         EntityModifier.insertChild(state, kindPhone, typeHome);
    225         EntityModifier.insertChild(state, kindPhone, typeHome);
    226 
    227         // Expecting none
    228         validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
    229         assertNotContains(validTypes, typeHome);
    230         assertNotContains(validTypes, typeWork);
    231         assertNotContains(validTypes, typeOther);
    232     }
    233 
    234     /**
    235      * Test {@link EntityModifier#canInsert(EntityDelta, DataKind)} by
    236      * inserting various rows.
    237      */
    238     public void testCanInsert() {
    239         // Build a source and pull specific types
    240         final ContactsSource source = getSource();
    241         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    242         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    243         final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
    244         final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
    245 
    246         // Add first home, first work
    247         final EntityDelta state = getEntity(TEST_ID);
    248         EntityModifier.insertChild(state, kindPhone, typeHome);
    249         EntityModifier.insertChild(state, kindPhone, typeWork);
    250         assertTrue("Unable to insert", EntityModifier.canInsert(state, kindPhone));
    251 
    252         // Add two other, which puts us just under "5" overall limit
    253         EntityModifier.insertChild(state, kindPhone, typeOther);
    254         EntityModifier.insertChild(state, kindPhone, typeOther);
    255         assertTrue("Unable to insert", EntityModifier.canInsert(state, kindPhone));
    256 
    257         // Add second home, which should push to snug limit
    258         EntityModifier.insertChild(state, kindPhone, typeHome);
    259         assertFalse("Able to insert", EntityModifier.canInsert(state, kindPhone));
    260     }
    261 
    262     /**
    263      * Test
    264      * {@link EntityModifier#getBestValidType(EntityDelta, DataKind, boolean, int)}
    265      * by asserting expected best options in various states.
    266      */
    267     public void testBestValidType() {
    268         // Build a source and pull specific types
    269         final ContactsSource source = getSource();
    270         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    271         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    272         final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
    273         final EditType typeFaxWork = EntityModifier.getType(kindPhone, Phone.TYPE_FAX_WORK);
    274         final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
    275 
    276         EditType suggested;
    277 
    278         // Default suggestion should be home
    279         final EntityDelta state = getEntity(TEST_ID);
    280         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
    281         assertEquals("Unexpected suggestion", typeHome, suggested);
    282 
    283         // Add first home, should now suggest work
    284         EntityModifier.insertChild(state, kindPhone, typeHome);
    285         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
    286         assertEquals("Unexpected suggestion", typeWork, suggested);
    287 
    288         // Add work fax, should still suggest work
    289         EntityModifier.insertChild(state, kindPhone, typeFaxWork);
    290         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
    291         assertEquals("Unexpected suggestion", typeWork, suggested);
    292 
    293         // Add other, should still suggest work
    294         EntityModifier.insertChild(state, kindPhone, typeOther);
    295         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
    296         assertEquals("Unexpected suggestion", typeWork, suggested);
    297 
    298         // Add work, now should suggest other
    299         EntityModifier.insertChild(state, kindPhone, typeWork);
    300         suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
    301         assertEquals("Unexpected suggestion", typeOther, suggested);
    302     }
    303 
    304     public void testIsEmptyEmpty() {
    305         final ContactsSource source = getSource();
    306         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    307 
    308         // Test entirely empty row
    309         final ContentValues after = new ContentValues();
    310         final ValuesDelta values = ValuesDelta.fromAfter(after);
    311 
    312         assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
    313     }
    314 
    315     public void testIsEmptyDirectFields() {
    316         final ContactsSource source = getSource();
    317         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    318         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    319 
    320         // Test row that has type values, but core fields are empty
    321         final EntityDelta state = getEntity(TEST_ID);
    322         final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
    323 
    324         assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
    325 
    326         // Insert some data to trigger non-empty state
    327         values.put(Phone.NUMBER, TEST_PHONE);
    328 
    329         assertFalse("Expected non-empty", EntityModifier.isEmpty(values, kindPhone));
    330     }
    331 
    332     public void testTrimEmptySingle() {
    333         final ContactsSource source = getSource();
    334         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    335         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    336 
    337         // Test row that has type values, but core fields are empty
    338         final EntityDelta state = getEntity(TEST_ID);
    339         final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
    340 
    341         // Build diff, expecting insert for data row and update enforcement
    342         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    343         state.buildDiff(diff);
    344         assertEquals("Unexpected operations", 3, diff.size());
    345         {
    346             final ContentProviderOperation oper = diff.get(0);
    347             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    348             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    349         }
    350         {
    351             final ContentProviderOperation oper = diff.get(1);
    352             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
    353             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    354         }
    355         {
    356             final ContentProviderOperation oper = diff.get(2);
    357             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    358             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    359         }
    360 
    361         // Trim empty rows and try again, expecting delete of overall contact
    362         EntityModifier.trimEmpty(state, source);
    363         diff.clear();
    364         state.buildDiff(diff);
    365         assertEquals("Unexpected operations", 1, diff.size());
    366         {
    367             final ContentProviderOperation oper = diff.get(0);
    368             assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
    369             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    370         }
    371     }
    372 
    373     public void testTrimEmptySpaces() {
    374         final ContactsSource source = getSource();
    375         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    376         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    377 
    378         // Test row that has type values, but values are spaces
    379         final EntityDelta state = EntitySetTests.buildBeforeEntity(TEST_ID, VER_FIRST);
    380         final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
    381         values.put(Phone.NUMBER, "   ");
    382 
    383         // Build diff, expecting insert for data row and update enforcement
    384         EntitySetTests.assertDiffPattern(state,
    385                 EntitySetTests.buildAssertVersion(VER_FIRST),
    386                 EntitySetTests.buildUpdateAggregationSuspended(),
    387                 EntitySetTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
    388                         EntitySetTests.buildDataInsert(values, TEST_ID)),
    389                 EntitySetTests.buildUpdateAggregationDefault());
    390 
    391         // Trim empty rows and try again, expecting delete of overall contact
    392         EntityModifier.trimEmpty(state, source);
    393         EntitySetTests.assertDiffPattern(state,
    394                 EntitySetTests.buildAssertVersion(VER_FIRST),
    395                 EntitySetTests.buildDelete(RawContacts.CONTENT_URI));
    396     }
    397 
    398     public void testTrimLeaveValid() {
    399         final ContactsSource source = getSource();
    400         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    401         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    402 
    403         // Test row that has type values with valid number
    404         final EntityDelta state = EntitySetTests.buildBeforeEntity(TEST_ID, VER_FIRST);
    405         final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
    406         values.put(Phone.NUMBER, TEST_PHONE);
    407 
    408         // Build diff, expecting insert for data row and update enforcement
    409         EntitySetTests.assertDiffPattern(state,
    410                 EntitySetTests.buildAssertVersion(VER_FIRST),
    411                 EntitySetTests.buildUpdateAggregationSuspended(),
    412                 EntitySetTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
    413                         EntitySetTests.buildDataInsert(values, TEST_ID)),
    414                 EntitySetTests.buildUpdateAggregationDefault());
    415 
    416         // Trim empty rows and try again, expecting no differences
    417         EntityModifier.trimEmpty(state, source);
    418         EntitySetTests.assertDiffPattern(state,
    419                 EntitySetTests.buildAssertVersion(VER_FIRST),
    420                 EntitySetTests.buildUpdateAggregationSuspended(),
    421                 EntitySetTests.buildOper(Data.CONTENT_URI, TYPE_INSERT,
    422                         EntitySetTests.buildDataInsert(values, TEST_ID)),
    423                 EntitySetTests.buildUpdateAggregationDefault());
    424     }
    425 
    426     public void testTrimEmptyUntouched() {
    427         final ContactsSource source = getSource();
    428         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    429         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    430 
    431         // Build "before" that has empty row
    432         final EntityDelta state = getEntity(TEST_ID);
    433         final ContentValues before = new ContentValues();
    434         before.put(Data._ID, TEST_ID);
    435         before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    436         state.addEntry(ValuesDelta.fromBefore(before));
    437 
    438         // Build diff, expecting no changes
    439         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    440         state.buildDiff(diff);
    441         assertEquals("Unexpected operations", 0, diff.size());
    442 
    443         // Try trimming existing empty, which we shouldn't touch
    444         EntityModifier.trimEmpty(state, source);
    445         diff.clear();
    446         state.buildDiff(diff);
    447         assertEquals("Unexpected operations", 0, diff.size());
    448     }
    449 
    450     public void testTrimEmptyAfterUpdate() {
    451         final ContactsSource source = getSource();
    452         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    453         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    454 
    455         // Build "before" that has row with some phone number
    456         final ContentValues before = new ContentValues();
    457         before.put(Data._ID, TEST_ID);
    458         before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    459         before.put(kindPhone.typeColumn, typeHome.rawValue);
    460         before.put(Phone.NUMBER, TEST_PHONE);
    461         final EntityDelta state = getEntity(TEST_ID, before);
    462 
    463         // Build diff, expecting no changes
    464         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    465         state.buildDiff(diff);
    466         assertEquals("Unexpected operations", 0, diff.size());
    467 
    468         // Now update row by changing number to empty string, expecting single update
    469         final ValuesDelta child = state.getEntry(TEST_ID);
    470         child.put(Phone.NUMBER, "");
    471         diff.clear();
    472         state.buildDiff(diff);
    473         assertEquals("Unexpected operations", 3, diff.size());
    474         {
    475             final ContentProviderOperation oper = diff.get(0);
    476             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    477             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    478         }
    479         {
    480             final ContentProviderOperation oper = diff.get(1);
    481             assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
    482             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    483         }
    484         {
    485             final ContentProviderOperation oper = diff.get(2);
    486             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    487             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    488         }
    489 
    490         // Now run trim, which should turn that update into delete
    491         EntityModifier.trimEmpty(state, source);
    492         diff.clear();
    493         state.buildDiff(diff);
    494         assertEquals("Unexpected operations", 1, diff.size());
    495         {
    496             final ContentProviderOperation oper = diff.get(0);
    497             assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
    498             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    499         }
    500     }
    501 
    502     public void testTrimInsertEmpty() {
    503         final ContactsSource source = getSource();
    504         final Sources sources = getSources(source);
    505         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    506         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    507 
    508         // Try creating a contact without any child entries
    509         final EntityDelta state = getEntity(null);
    510         final EntitySet set = EntitySet.fromSingle(state);
    511 
    512         // Build diff, expecting single insert
    513         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    514         state.buildDiff(diff);
    515         assertEquals("Unexpected operations", 2, diff.size());
    516         {
    517             final ContentProviderOperation oper = diff.get(0);
    518             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
    519             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    520         }
    521 
    522         // Trim empty rows and try again, expecting no insert
    523         EntityModifier.trimEmpty(set, sources);
    524         diff.clear();
    525         state.buildDiff(diff);
    526         assertEquals("Unexpected operations", 0, diff.size());
    527     }
    528 
    529     public void testTrimInsertInsert() {
    530         final ContactsSource source = getSource();
    531         final Sources sources = getSources(source);
    532         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    533         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    534 
    535         // Try creating a contact with single empty entry
    536         final EntityDelta state = getEntity(null);
    537         final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
    538         final EntitySet set = EntitySet.fromSingle(state);
    539 
    540         // Build diff, expecting two insert operations
    541         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    542         state.buildDiff(diff);
    543         assertEquals("Unexpected operations", 3, diff.size());
    544         {
    545             final ContentProviderOperation oper = diff.get(0);
    546             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
    547             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    548         }
    549         {
    550             final ContentProviderOperation oper = diff.get(1);
    551             assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
    552             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    553         }
    554 
    555         // Trim empty rows and try again, expecting silence
    556         EntityModifier.trimEmpty(set, sources);
    557         diff.clear();
    558         state.buildDiff(diff);
    559         assertEquals("Unexpected operations", 0, diff.size());
    560     }
    561 
    562     public void testTrimUpdateRemain() {
    563         final ContactsSource source = getSource();
    564         final Sources sources = getSources(source);
    565         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    566         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    567 
    568         // Build "before" with two phone numbers
    569         final ContentValues first = new ContentValues();
    570         first.put(Data._ID, TEST_ID);
    571         first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    572         first.put(kindPhone.typeColumn, typeHome.rawValue);
    573         first.put(Phone.NUMBER, TEST_PHONE);
    574 
    575         final ContentValues second = new ContentValues();
    576         second.put(Data._ID, TEST_ID);
    577         second.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    578         second.put(kindPhone.typeColumn, typeHome.rawValue);
    579         second.put(Phone.NUMBER, TEST_PHONE);
    580 
    581         final EntityDelta state = getEntity(TEST_ID, first, second);
    582         final EntitySet set = EntitySet.fromSingle(state);
    583 
    584         // Build diff, expecting no changes
    585         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    586         state.buildDiff(diff);
    587         assertEquals("Unexpected operations", 0, diff.size());
    588 
    589         // Now update row by changing number to empty string, expecting single update
    590         final ValuesDelta child = state.getEntry(TEST_ID);
    591         child.put(Phone.NUMBER, "");
    592         diff.clear();
    593         state.buildDiff(diff);
    594         assertEquals("Unexpected operations", 3, diff.size());
    595         {
    596             final ContentProviderOperation oper = diff.get(0);
    597             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    598             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    599         }
    600         {
    601             final ContentProviderOperation oper = diff.get(1);
    602             assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
    603             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    604         }
    605         {
    606             final ContentProviderOperation oper = diff.get(2);
    607             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    608             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    609         }
    610 
    611         // Now run trim, which should turn that update into delete
    612         EntityModifier.trimEmpty(set, sources);
    613         diff.clear();
    614         state.buildDiff(diff);
    615         assertEquals("Unexpected operations", 3, diff.size());
    616         {
    617             final ContentProviderOperation oper = diff.get(0);
    618             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    619             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    620         }
    621         {
    622             final ContentProviderOperation oper = diff.get(1);
    623             assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
    624             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    625         }
    626         {
    627             final ContentProviderOperation oper = diff.get(2);
    628             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    629             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    630         }
    631     }
    632 
    633     public void testTrimUpdateUpdate() {
    634         final ContactsSource source = getSource();
    635         final Sources sources = getSources(source);
    636         final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
    637         final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
    638 
    639         // Build "before" with two phone numbers
    640         final ContentValues first = new ContentValues();
    641         first.put(Data._ID, TEST_ID);
    642         first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    643         first.put(kindPhone.typeColumn, typeHome.rawValue);
    644         first.put(Phone.NUMBER, TEST_PHONE);
    645 
    646         final EntityDelta state = getEntity(TEST_ID, first);
    647         final EntitySet set = EntitySet.fromSingle(state);
    648 
    649         // Build diff, expecting no changes
    650         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    651         state.buildDiff(diff);
    652         assertEquals("Unexpected operations", 0, diff.size());
    653 
    654         // Now update row by changing number to empty string, expecting single update
    655         final ValuesDelta child = state.getEntry(TEST_ID);
    656         child.put(Phone.NUMBER, "");
    657         diff.clear();
    658         state.buildDiff(diff);
    659         assertEquals("Unexpected operations", 3, diff.size());
    660         {
    661             final ContentProviderOperation oper = diff.get(0);
    662             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    663             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    664         }
    665         {
    666             final ContentProviderOperation oper = diff.get(1);
    667             assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
    668             assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
    669         }
    670         {
    671             final ContentProviderOperation oper = diff.get(2);
    672             assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
    673             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    674         }
    675 
    676         // Now run trim, which should turn into deleting the whole contact
    677         EntityModifier.trimEmpty(set, sources);
    678         diff.clear();
    679         state.buildDiff(diff);
    680         assertEquals("Unexpected operations", 1, diff.size());
    681         {
    682             final ContentProviderOperation oper = diff.get(0);
    683             assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
    684             assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
    685         }
    686     }
    687 
    688     public void testParseExtrasExistingName() {
    689         final ContactsSource source = getSource();
    690         final DataKind kindName = source.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
    691 
    692         // Build "before" name
    693         final ContentValues first = new ContentValues();
    694         first.put(Data._ID, TEST_ID);
    695         first.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    696         first.put(StructuredName.GIVEN_NAME, TEST_NAME);
    697 
    698         // Parse extras, making sure we keep single name
    699         final EntityDelta state = getEntity(TEST_ID, first);
    700         final Bundle extras = new Bundle();
    701         extras.putString(Insert.NAME, TEST_NAME2);
    702         EntityModifier.parseExtras(mContext, source, state, extras);
    703 
    704         final int nameCount = state.getMimeEntriesCount(StructuredName.CONTENT_ITEM_TYPE, true);
    705         assertEquals("Unexpected names", 1, nameCount);
    706     }
    707 
    708     public void testParseExtrasIgnoreLimit() {
    709         final ContactsSource source = getSource();
    710         final DataKind kindIm = source.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
    711 
    712         // Build "before" IM
    713         final ContentValues first = new ContentValues();
    714         first.put(Data._ID, TEST_ID);
    715         first.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
    716         first.put(Im.DATA, TEST_IM);
    717 
    718         final EntityDelta state = getEntity(TEST_ID, first);
    719         final int beforeCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
    720 
    721         // We should ignore data that doesn't fit source rules, since source
    722         // only allows single Im
    723         final Bundle extras = new Bundle();
    724         extras.putInt(Insert.IM_PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
    725         extras.putString(Insert.IM_HANDLE, TEST_IM);
    726         EntityModifier.parseExtras(mContext, source, state, extras);
    727 
    728         final int afterCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
    729         assertEquals("Broke source rules", beforeCount, afterCount);
    730     }
    731 
    732     public void testParseExtrasIgnoreUnhandled() {
    733         final ContactsSource source = getSource();
    734         final EntityDelta state = getEntity(TEST_ID);
    735 
    736         // We should silently ignore types unsupported by source
    737         final Bundle extras = new Bundle();
    738         extras.putString(Insert.POSTAL, TEST_POSTAL);
    739         EntityModifier.parseExtras(mContext, source, state, extras);
    740 
    741         assertNull("Broke source rules", state.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
    742     }
    743 
    744     public void testParseExtrasJobTitle() {
    745         final ContactsSource source = getSource();
    746         final EntityDelta state = getEntity(TEST_ID);
    747 
    748         // Make sure that we create partial Organizations
    749         final Bundle extras = new Bundle();
    750         extras.putString(Insert.JOB_TITLE, TEST_NAME);
    751         EntityModifier.parseExtras(mContext, source, state, extras);
    752 
    753         final int count = state.getMimeEntries(Organization.CONTENT_ITEM_TYPE).size();
    754         assertEquals("Expected to create organization", 1, count);
    755     }
    756 }
    757