Home | History | Annotate | Download | only in adapter
      1 package com.android.exchange.adapter;
      2 
      3 import android.content.ContentProviderOperation;
      4 import android.content.ContentProviderOperation.Builder;
      5 import android.content.ContentProviderResult;
      6 import android.content.ContentResolver;
      7 import android.content.ContentUris;
      8 import android.content.ContentValues;
      9 import android.content.Context;
     10 import android.content.Entity;
     11 import android.content.Entity.NamedContentValues;
     12 import android.content.EntityIterator;
     13 import android.content.OperationApplicationException;
     14 import android.database.Cursor;
     15 import android.net.Uri;
     16 import android.os.RemoteException;
     17 import android.provider.ContactsContract;
     18 import android.provider.ContactsContract.CommonDataKinds.Email;
     19 import android.provider.ContactsContract.CommonDataKinds.Event;
     20 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     21 import android.provider.ContactsContract.CommonDataKinds.Im;
     22 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     23 import android.provider.ContactsContract.CommonDataKinds.Note;
     24 import android.provider.ContactsContract.CommonDataKinds.Organization;
     25 import android.provider.ContactsContract.CommonDataKinds.Phone;
     26 import android.provider.ContactsContract.CommonDataKinds.Photo;
     27 import android.provider.ContactsContract.CommonDataKinds.Relation;
     28 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     29 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     30 import android.provider.ContactsContract.CommonDataKinds.Website;
     31 import android.provider.ContactsContract.Data;
     32 import android.provider.ContactsContract.RawContacts;
     33 import android.provider.ContactsContract.SyncState;
     34 import android.provider.SyncStateContract;
     35 import android.text.util.Rfc822Token;
     36 import android.text.util.Rfc822Tokenizer;
     37 import android.util.Base64;
     38 
     39 import com.android.emailcommon.provider.Account;
     40 import com.android.emailcommon.provider.Mailbox;
     41 import com.android.emailcommon.utility.Utility;
     42 import com.android.exchange.Eas;
     43 import com.android.exchange.eas.EasSyncCollectionTypeBase;
     44 import com.android.exchange.eas.EasSyncContacts;
     45 import com.android.exchange.utility.CalendarUtilities;
     46 import com.android.mail.utils.LogUtils;
     47 
     48 import java.io.IOException;
     49 import java.io.InputStream;
     50 import java.util.ArrayList;
     51 import java.util.GregorianCalendar;
     52 import java.util.TimeZone;
     53 
     54 public class ContactsSyncParser extends AbstractSyncParser {
     55     private static final String TAG = Eas.LOG_TAG;
     56 
     57     private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
     58     private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
     59     private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
     60 
     61     private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
     62         = new ArrayList<NamedContentValues>();
     63 
     64     private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
     65 
     66     private static final int MAX_IM_ROWS = 3;
     67     private static final int MAX_EMAIL_ROWS = 3;
     68     private static final int MAX_PHONE_ROWS = 2;
     69     private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
     70     private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
     71 
     72     String[] mBindArgument = new String[1];
     73     ContactOperations ops = new ContactOperations();
     74     private final android.accounts.Account mAccountManagerAccount;
     75     private final Uri mAccountUri;
     76     private boolean mGroupsUsed = false;
     77 
     78     public ContactsSyncParser(final Context context, final ContentResolver resolver,
     79             final InputStream in, final Mailbox mailbox, final Account account,
     80             final android.accounts.Account accountManagerAccount) throws IOException {
     81         super(context, resolver, in, mailbox, account);
     82         mAccountManagerAccount = accountManagerAccount;
     83         mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI,
     84                 mAccount.mEmailAddress);
     85     }
     86 
     87     public boolean isGroupsUsed() {
     88         return mGroupsUsed;
     89     }
     90 
     91     public void addData(String serverId, ContactOperations ops, Entity entity)
     92             throws IOException {
     93         String prefix = null;
     94         String firstName = null;
     95         String lastName = null;
     96         String middleName = null;
     97         String suffix = null;
     98         String companyName = null;
     99         String yomiFirstName = null;
    100         String yomiLastName = null;
    101         String yomiCompanyName = null;
    102         String title = null;
    103         String department = null;
    104         String officeLocation = null;
    105         Address home = new Address();
    106         Address work = new Address();
    107         Address other = new Address();
    108         EasBusiness business = new EasBusiness();
    109         EasPersonal personal = new EasPersonal();
    110         ArrayList<String> children = new ArrayList<String>();
    111         ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
    112         ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
    113         ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
    114         ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
    115         if (entity == null) {
    116             ops.newContact(serverId, mAccount.mEmailAddress);
    117         }
    118 
    119         while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
    120             switch (tag) {
    121                 case Tags.CONTACTS_FIRST_NAME:
    122                     firstName = getValue();
    123                     break;
    124                 case Tags.CONTACTS_LAST_NAME:
    125                     lastName = getValue();
    126                     break;
    127                 case Tags.CONTACTS_MIDDLE_NAME:
    128                     middleName = getValue();
    129                     break;
    130                 case Tags.CONTACTS_SUFFIX:
    131                     suffix = getValue();
    132                     break;
    133                 case Tags.CONTACTS_COMPANY_NAME:
    134                     companyName = getValue();
    135                     break;
    136                 case Tags.CONTACTS_JOB_TITLE:
    137                     title = getValue();
    138                     break;
    139                 case Tags.CONTACTS_EMAIL1_ADDRESS:
    140                 case Tags.CONTACTS_EMAIL2_ADDRESS:
    141                 case Tags.CONTACTS_EMAIL3_ADDRESS:
    142                     emails.add(new EmailRow(getValue()));
    143                     break;
    144                 case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
    145                 case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
    146                     workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
    147                     break;
    148                 case Tags.CONTACTS2_MMS:
    149                     ops.addPhone(entity, Phone.TYPE_MMS, getValue());
    150                     break;
    151                 case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
    152                     ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
    153                     break;
    154                 case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
    155                     ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
    156                     break;
    157                 case Tags.CONTACTS_HOME_FAX_NUMBER:
    158                     ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
    159                     break;
    160                 case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
    161                 case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
    162                     homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
    163                     break;
    164                 case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
    165                     ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
    166                     break;
    167                 case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
    168                     ops.addPhone(entity, Phone.TYPE_CAR, getValue());
    169                     break;
    170                 case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
    171                     ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
    172                     break;
    173                 case Tags.CONTACTS_PAGER_NUMBER:
    174                     ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
    175                     break;
    176                 case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
    177                     ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
    178                     break;
    179                 case Tags.CONTACTS2_IM_ADDRESS:
    180                 case Tags.CONTACTS2_IM_ADDRESS_2:
    181                 case Tags.CONTACTS2_IM_ADDRESS_3:
    182                     ims.add(new ImRow(getValue()));
    183                     break;
    184                 case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
    185                     work.city = getValue();
    186                     break;
    187                 case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
    188                     work.country = getValue();
    189                     break;
    190                 case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
    191                     work.code = getValue();
    192                     break;
    193                 case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
    194                     work.state = getValue();
    195                     break;
    196                 case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
    197                     work.street = getValue();
    198                     break;
    199                 case Tags.CONTACTS_HOME_ADDRESS_CITY:
    200                     home.city = getValue();
    201                     break;
    202                 case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
    203                     home.country = getValue();
    204                     break;
    205                 case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
    206                     home.code = getValue();
    207                     break;
    208                 case Tags.CONTACTS_HOME_ADDRESS_STATE:
    209                     home.state = getValue();
    210                     break;
    211                 case Tags.CONTACTS_HOME_ADDRESS_STREET:
    212                     home.street = getValue();
    213                     break;
    214                 case Tags.CONTACTS_OTHER_ADDRESS_CITY:
    215                     other.city = getValue();
    216                     break;
    217                 case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
    218                     other.country = getValue();
    219                     break;
    220                 case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
    221                     other.code = getValue();
    222                     break;
    223                 case Tags.CONTACTS_OTHER_ADDRESS_STATE:
    224                     other.state = getValue();
    225                     break;
    226                 case Tags.CONTACTS_OTHER_ADDRESS_STREET:
    227                     other.street = getValue();
    228                     break;
    229 
    230                 case Tags.CONTACTS_CHILDREN:
    231                     childrenParser(children);
    232                     break;
    233 
    234                 case Tags.CONTACTS_YOMI_COMPANY_NAME:
    235                     yomiCompanyName = getValue();
    236                     break;
    237                 case Tags.CONTACTS_YOMI_FIRST_NAME:
    238                     yomiFirstName = getValue();
    239                     break;
    240                 case Tags.CONTACTS_YOMI_LAST_NAME:
    241                     yomiLastName = getValue();
    242                     break;
    243 
    244                 case Tags.CONTACTS2_NICKNAME:
    245                     ops.addNickname(entity, getValue());
    246                     break;
    247 
    248                 case Tags.CONTACTS_ASSISTANT_NAME:
    249                     ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
    250                     break;
    251                 case Tags.CONTACTS2_MANAGER_NAME:
    252                     ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
    253                     break;
    254                 case Tags.CONTACTS_SPOUSE:
    255                     ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
    256                     break;
    257                 case Tags.CONTACTS_DEPARTMENT:
    258                     department = getValue();
    259                     break;
    260                 case Tags.CONTACTS_TITLE:
    261                     prefix = getValue();
    262                     break;
    263 
    264                 // EAS Business
    265                 case Tags.CONTACTS_OFFICE_LOCATION:
    266                     officeLocation = getValue();
    267                     break;
    268                 case Tags.CONTACTS2_CUSTOMER_ID:
    269                     business.customerId = getValue();
    270                     break;
    271                 case Tags.CONTACTS2_GOVERNMENT_ID:
    272                     business.governmentId = getValue();
    273                     break;
    274                 case Tags.CONTACTS2_ACCOUNT_NAME:
    275                     business.accountName = getValue();
    276                     break;
    277 
    278                 // EAS Personal
    279                 case Tags.CONTACTS_ANNIVERSARY:
    280                     personal.anniversary = getValue();
    281                     break;
    282                 case Tags.CONTACTS_BIRTHDAY:
    283                     ops.addBirthday(entity, getValue());
    284                     break;
    285                 case Tags.CONTACTS_WEBPAGE:
    286                     ops.addWebpage(entity, getValue());
    287                     break;
    288 
    289                 case Tags.CONTACTS_PICTURE:
    290                     ops.addPhoto(entity, getValue());
    291                     break;
    292 
    293                 case Tags.BASE_BODY:
    294                     ops.addNote(entity, bodyParser());
    295                     break;
    296                 case Tags.CONTACTS_BODY:
    297                     ops.addNote(entity, getValue());
    298                     break;
    299 
    300                 case Tags.CONTACTS_CATEGORIES:
    301                     mGroupsUsed = true;
    302                     categoriesParser(ops, entity);
    303                     break;
    304 
    305                 default:
    306                     skipTag();
    307             }
    308         }
    309 
    310         ops.addName(entity, prefix, firstName, lastName, middleName, suffix,
    311                 yomiFirstName, yomiLastName);
    312         ops.addBusiness(entity, business);
    313         ops.addPersonal(entity, personal);
    314 
    315         ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
    316         ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
    317         ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
    318                 MAX_PHONE_ROWS);
    319         ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
    320                 MAX_PHONE_ROWS);
    321 
    322         if (!children.isEmpty()) {
    323             ops.addChildren(entity, children);
    324         }
    325 
    326         if (work.hasData()) {
    327             ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
    328                     work.state, work.country, work.code);
    329         }
    330         if (home.hasData()) {
    331             ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
    332                     home.state, home.country, home.code);
    333         }
    334         if (other.hasData()) {
    335             ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
    336                     other.state, other.country, other.code);
    337         }
    338 
    339         if (companyName != null) {
    340             ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
    341                     yomiCompanyName, officeLocation);
    342         }
    343 
    344         if (entity != null) {
    345             // We've been removing rows from the list as they've been found in the xml
    346             // Any that are left must have been deleted on the server
    347             ArrayList<NamedContentValues> ncvList = entity.getSubValues();
    348             for (NamedContentValues ncv: ncvList) {
    349                 // These rows need to be deleted...
    350                 Uri u = dataUriFromNamedContentValues(ncv);
    351                 ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
    352                         .build());
    353             }
    354         }
    355     }
    356 
    357     private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
    358         while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
    359             switch (tag) {
    360                 case Tags.CONTACTS_CATEGORY:
    361                     ops.addGroup(entity, getValue());
    362                     break;
    363                 default:
    364                     skipTag();
    365             }
    366         }
    367     }
    368 
    369     private void childrenParser(ArrayList<String> children) throws IOException {
    370         while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
    371             switch (tag) {
    372                 case Tags.CONTACTS_CHILD:
    373                     if (children.size() < EasChildren.MAX_CHILDREN) {
    374                         children.add(getValue());
    375                     }
    376                     break;
    377                 default:
    378                     skipTag();
    379             }
    380         }
    381     }
    382 
    383     private String bodyParser() throws IOException {
    384         String body = null;
    385         while (nextTag(Tags.BASE_BODY) != END) {
    386             switch (tag) {
    387                 case Tags.BASE_DATA:
    388                     body = getValue();
    389                     break;
    390                 default:
    391                     skipTag();
    392             }
    393         }
    394         return body;
    395     }
    396 
    397     public void addParser(ContactOperations ops) throws IOException {
    398         String serverId = null;
    399         while (nextTag(Tags.SYNC_ADD) != END) {
    400             switch (tag) {
    401                 case Tags.SYNC_SERVER_ID: // same as
    402                     serverId = getValue();
    403                     break;
    404                 case Tags.SYNC_APPLICATION_DATA:
    405                     addData(serverId, ops, null);
    406                     break;
    407                 default:
    408                     skipTag();
    409             }
    410         }
    411     }
    412 
    413     private Cursor getServerIdCursor(String serverId) {
    414         mBindArgument[0] = serverId;
    415         return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
    416                 mBindArgument, null);
    417     }
    418 
    419     private Cursor getClientIdCursor(String clientId) {
    420         mBindArgument[0] = clientId;
    421         return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
    422                 mBindArgument, null);
    423     }
    424 
    425     public void deleteParser(ContactOperations ops) throws IOException {
    426         while (nextTag(Tags.SYNC_DELETE) != END) {
    427             switch (tag) {
    428                 case Tags.SYNC_SERVER_ID:
    429                     String serverId = getValue();
    430                     // Find the message in this mailbox with the given serverId
    431                     Cursor c = getServerIdCursor(serverId);
    432                     try {
    433                         if (c.moveToFirst()) {
    434                             userLog("Deleting ", serverId);
    435                             ops.delete(c.getLong(0));
    436                         }
    437                     } finally {
    438                         c.close();
    439                     }
    440                     break;
    441                 default:
    442                     skipTag();
    443             }
    444         }
    445     }
    446 
    447     class ServerChange {
    448         long id;
    449         boolean read;
    450 
    451         ServerChange(long _id, boolean _read) {
    452             id = _id;
    453             read = _read;
    454         }
    455     }
    456 
    457     /**
    458      * Changes are handled row by row, and only changed/new rows are acted upon
    459      * @param ops the array of pending ContactProviderOperations.
    460      * @throws IOException
    461      */
    462     public void changeParser(ContactOperations ops) throws IOException {
    463         String serverId = null;
    464         Entity entity = null;
    465         while (nextTag(Tags.SYNC_CHANGE) != END) {
    466             switch (tag) {
    467                 case Tags.SYNC_SERVER_ID:
    468                     serverId = getValue();
    469                     Cursor c = getServerIdCursor(serverId);
    470                     try {
    471                         if (c.moveToFirst()) {
    472                             // TODO Handle deleted individual rows...
    473                             Uri uri = ContentUris.withAppendedId(
    474                                     RawContacts.CONTENT_URI, c.getLong(0));
    475                             uri = Uri.withAppendedPath(
    476                                     uri, RawContacts.Entity.CONTENT_DIRECTORY);
    477                             EntityIterator entityIterator = RawContacts.newEntityIterator(
    478                                 mContentResolver.query(uri, null, null, null, null));
    479                             if (entityIterator.hasNext()) {
    480                                 entity = entityIterator.next();
    481                             }
    482                             userLog("Changing contact ", serverId);
    483                         }
    484                     } finally {
    485                         c.close();
    486                     }
    487                     break;
    488                 case Tags.SYNC_APPLICATION_DATA:
    489                     addData(serverId, ops, entity);
    490                     break;
    491                 default:
    492                     skipTag();
    493             }
    494         }
    495     }
    496 
    497     @Override
    498     public void commandsParser() throws IOException {
    499         while (nextTag(Tags.SYNC_COMMANDS) != END) {
    500             if (tag == Tags.SYNC_ADD) {
    501                 addParser(ops);
    502             } else if (tag == Tags.SYNC_DELETE) {
    503                 deleteParser(ops);
    504             } else if (tag == Tags.SYNC_CHANGE) {
    505                 changeParser(ops);
    506             } else
    507                 skipTag();
    508         }
    509     }
    510 
    511     @Override
    512     public void commit() throws IOException {
    513        // Save the syncKey here, using the Helper provider by Contacts provider
    514         userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
    515         ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
    516                 mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
    517 
    518         // Execute these all at once...
    519         ops.execute(mContext);
    520 
    521         if (ops.mResults != null) {
    522             ContentValues cv = new ContentValues();
    523             cv.put(RawContacts.DIRTY, 0);
    524             for (int i = 0; i < ops.mContactIndexCount; i++) {
    525                 int index = ops.mContactIndexArray[i];
    526                 Uri u = ops.mResults[index].uri;
    527                 if (u != null) {
    528                     String idString = u.getLastPathSegment();
    529                     mContentResolver.update(
    530                             addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
    531                             RawContacts._ID + "=" + idString, null);
    532                 }
    533             }
    534         }
    535     }
    536 
    537     public void addResponsesParser() throws IOException {
    538         String serverId = null;
    539         String clientId = null;
    540         ContentValues cv = new ContentValues();
    541         while (nextTag(Tags.SYNC_ADD) != END) {
    542             switch (tag) {
    543                 case Tags.SYNC_SERVER_ID:
    544                     serverId = getValue();
    545                     break;
    546                 case Tags.SYNC_CLIENT_ID:
    547                     clientId = getValue();
    548                     break;
    549                 case Tags.SYNC_STATUS:
    550                     getValue();
    551                     break;
    552                 default:
    553                     skipTag();
    554             }
    555         }
    556 
    557         // This is theoretically impossible, but...
    558         if (clientId == null || serverId == null) return;
    559 
    560         Cursor c = getClientIdCursor(clientId);
    561         try {
    562             if (c.moveToFirst()) {
    563                 cv.put(RawContacts.SOURCE_ID, serverId);
    564                 cv.put(RawContacts.DIRTY, 0);
    565                 ops.add(ContentProviderOperation.newUpdate(
    566                         ContentUris.withAppendedId(
    567                                 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
    568                                 c.getLong(0)))
    569                         .withValues(cv)
    570                         .build());
    571                 userLog("New contact " + clientId + " was given serverId: " + serverId);
    572             }
    573         } finally {
    574             c.close();
    575         }
    576     }
    577 
    578     public void changeResponsesParser() throws IOException {
    579         String serverId = null;
    580         String status = null;
    581         while (nextTag(Tags.SYNC_CHANGE) != END) {
    582             switch (tag) {
    583                 case Tags.SYNC_SERVER_ID:
    584                     serverId = getValue();
    585                     break;
    586                 case Tags.SYNC_STATUS:
    587                     status = getValue();
    588                     break;
    589                 default:
    590                     skipTag();
    591             }
    592         }
    593         if (serverId != null && status != null) {
    594             userLog("Changed contact " + serverId + " failed with status: " + status);
    595         }
    596     }
    597 
    598 
    599     @Override
    600     public void responsesParser() throws IOException {
    601         // Handle server responses here (for Add and Change)
    602         while (nextTag(Tags.SYNC_RESPONSES) != END) {
    603             if (tag == Tags.SYNC_ADD) {
    604                 addResponsesParser();
    605             } else if (tag == Tags.SYNC_CHANGE) {
    606                 changeResponsesParser();
    607             } else
    608                 skipTag();
    609         }
    610     }
    611 
    612     private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) {
    613         return uri.buildUpon()
    614             .appendQueryParameter(RawContacts.ACCOUNT_NAME, emailAddress)
    615             .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
    616             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    617             .build();
    618     }
    619 
    620     static Uri addCallerIsSyncAdapterParameter(Uri uri) {
    621         return uri.buildUpon()
    622                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    623                 .build();
    624     }
    625 
    626     /**
    627      * Generate the uri for the data row associated with this NamedContentValues object
    628      * @param ncv the NamedContentValues object
    629      * @return a uri that can be used to refer to this row
    630      */
    631     public static Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
    632         long id = ncv.values.getAsLong(RawContacts._ID);
    633         Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
    634         return dataUri;
    635     }
    636 
    637     public static final class EasChildren {
    638         private EasChildren() {}
    639 
    640         /** MIME type used when storing this in data table. */
    641         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
    642         public static final int MAX_CHILDREN = 8;
    643         public static final String[] ROWS =
    644             new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
    645     }
    646 
    647     public static final class EasPersonal {
    648         String anniversary;
    649         String fileAs;
    650 
    651             /** MIME type used when storing this in data table. */
    652         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
    653         public static final String ANNIVERSARY = "data2";
    654         public static final String FILE_AS = "data4";
    655 
    656         boolean hasData() {
    657             return anniversary != null || fileAs != null;
    658         }
    659     }
    660 
    661     public static final class EasBusiness {
    662         String customerId;
    663         String governmentId;
    664         String accountName;
    665 
    666         /** MIME type used when storing this in data table. */
    667         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
    668         public static final String CUSTOMER_ID = "data6";
    669         public static final String GOVERNMENT_ID = "data7";
    670         public static final String ACCOUNT_NAME = "data8";
    671 
    672         boolean hasData() {
    673             return customerId != null || governmentId != null || accountName != null;
    674         }
    675     }
    676 
    677     public static final class Address {
    678         String city;
    679         String country;
    680         String code;
    681         String street;
    682         String state;
    683 
    684         boolean hasData() {
    685             return city != null || country != null || code != null || state != null
    686                 || street != null;
    687         }
    688     }
    689 
    690     interface UntypedRow {
    691         public void addValues(RowBuilder builder);
    692         public boolean isSameAs(int type, String value);
    693     }
    694 
    695     static class EmailRow implements UntypedRow {
    696         String email;
    697         String displayName;
    698 
    699         public EmailRow(String _email) {
    700             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
    701             // Can't happen, but belt & suspenders
    702             if (tokens.length == 0) {
    703                 email = "";
    704                 displayName = "";
    705             } else {
    706                 Rfc822Token token = tokens[0];
    707                 email = token.getAddress();
    708                 displayName = token.getName();
    709             }
    710         }
    711 
    712         @Override
    713         public void addValues(RowBuilder builder) {
    714             builder.withValue(Email.DATA, email);
    715             builder.withValue(Email.DISPLAY_NAME, displayName);
    716         }
    717 
    718         @Override
    719         public boolean isSameAs(int type, String value) {
    720             return email.equalsIgnoreCase(value);
    721         }
    722     }
    723 
    724     static class ImRow implements UntypedRow {
    725         String im;
    726 
    727         public ImRow(String _im) {
    728             im = _im;
    729         }
    730 
    731         @Override
    732         public void addValues(RowBuilder builder) {
    733             builder.withValue(Im.DATA, im);
    734         }
    735 
    736         @Override
    737         public boolean isSameAs(int type, String value) {
    738             return im.equalsIgnoreCase(value);
    739         }
    740     }
    741 
    742     static class PhoneRow implements UntypedRow {
    743         String phone;
    744         int type;
    745 
    746         public PhoneRow(String _phone, int _type) {
    747             phone = _phone;
    748             type = _type;
    749         }
    750 
    751         @Override
    752         public void addValues(RowBuilder builder) {
    753             builder.withValue(Im.DATA, phone);
    754             builder.withValue(Phone.TYPE, type);
    755         }
    756 
    757         @Override
    758         public boolean isSameAs(int _type, String value) {
    759             return type == _type && phone.equalsIgnoreCase(value);
    760         }
    761     }
    762 
    763     /**
    764      * RowBuilder is a wrapper for the Builder class that is used to create/update rows for a
    765      * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
    766      * represent the current values of that row, that can be compared against current values to
    767      * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
    768      * the Builder.
    769      */
    770     private static class RowBuilder {
    771         Builder builder;
    772         ContentValues cv;
    773 
    774         public RowBuilder(Builder _builder) {
    775             builder = _builder;
    776         }
    777 
    778         public RowBuilder(Builder _builder, NamedContentValues _ncv) {
    779             builder = _builder;
    780             cv = _ncv.values;
    781         }
    782 
    783         RowBuilder withValueBackReference(String key, int previousResult) {
    784             builder.withValueBackReference(key, previousResult);
    785             return this;
    786         }
    787 
    788         ContentProviderOperation build() {
    789             return builder.build();
    790         }
    791 
    792         RowBuilder withValue(String key, Object value) {
    793             builder.withValue(key, value);
    794             return this;
    795         }
    796     }
    797     public static class ContactOperations extends ArrayList<ContentProviderOperation> {
    798         private static final long serialVersionUID = 1L;
    799         private int mCount = 0;
    800         private int mContactBackValue = mCount;
    801         // Make an array big enough for the max possible window size.
    802         private final int[] mContactIndexArray = new int[EasSyncCollectionTypeBase.MAX_WINDOW_SIZE];
    803         private int mContactIndexCount = 0;
    804         private ContentProviderResult[] mResults = null;
    805 
    806         @Override
    807         public boolean add(ContentProviderOperation op) {
    808             super.add(op);
    809             mCount++;
    810             return true;
    811         }
    812 
    813         public void newContact(final String serverId, final String emailAddress) {
    814             Builder builder = ContentProviderOperation.newInsert(
    815                     uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI, emailAddress));
    816             ContentValues values = new ContentValues();
    817             values.put(RawContacts.SOURCE_ID, serverId);
    818             builder.withValues(values);
    819             mContactBackValue = mCount;
    820             mContactIndexArray[mContactIndexCount++] = mCount;
    821             add(builder.build());
    822         }
    823 
    824         public void delete(long id) {
    825             add(ContentProviderOperation
    826                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
    827                             .buildUpon()
    828                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    829                             .build())
    830                     .build());
    831         }
    832 
    833         public void execute(final Context context) {
    834             try {
    835                 if (!isEmpty()) {
    836                     mResults = context.getContentResolver().applyBatch(
    837                             ContactsContract.AUTHORITY, this);
    838                 }
    839             } catch (RemoteException e) {
    840                 // There is nothing sensible to be done here
    841                 LogUtils.e(TAG, "problem inserting contact during server update", e);
    842             } catch (OperationApplicationException e) {
    843                 // There is nothing sensible to be done here
    844                 LogUtils.e(TAG, "problem inserting contact during server update", e);
    845             }
    846         }
    847 
    848         /**
    849          * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
    850          * tries to find a match, returning it
    851          * @param list the list of NCV's from the contact entity
    852          * @param contentItemType the mime type we're looking for
    853          * @param type the subtype (e.g. HOME, WORK, etc.)
    854          * @return the matching NCV or null if not found
    855          */
    856         private static NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
    857                 String contentItemType, int type, String stringType) {
    858             NamedContentValues result = null;
    859 
    860             // Loop through the ncv's, looking for an existing row
    861             for (NamedContentValues namedContentValues: list) {
    862                 Uri uri = namedContentValues.uri;
    863                 ContentValues cv = namedContentValues.values;
    864                 if (Data.CONTENT_URI.equals(uri)) {
    865                     String mimeType = cv.getAsString(Data.MIMETYPE);
    866                     if (mimeType.equals(contentItemType)) {
    867                         if (stringType != null) {
    868                             if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
    869                                 result = namedContentValues;
    870                             }
    871                         // Note Email.TYPE could be ANY type column; they are all defined in
    872                         // the private CommonColumns class in ContactsContract
    873                         // We'll accept either type < 0 (don't care), cv doesn't have a type,
    874                         // or the types are equal
    875                         } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
    876                                 cv.getAsInteger(Email.TYPE) == type) {
    877                             result = namedContentValues;
    878                         }
    879                     }
    880                 }
    881             }
    882 
    883             // If we've found an existing data row, we'll delete it.  Any rows left at the
    884             // end should be deleted...
    885             if (result != null) {
    886                 list.remove(result);
    887             }
    888 
    889             // Return the row found (or null)
    890             return result;
    891         }
    892 
    893         /**
    894          * Given the list of NamedContentValues for an entity and a mime type
    895          * gather all of the matching NCV's, returning them
    896          * @param list the list of NCV's from the contact entity
    897          * @param contentItemType the mime type we're looking for
    898          * @param type the subtype (e.g. HOME, WORK, etc.)
    899          * @return the matching NCVs
    900          */
    901         private static ArrayList<NamedContentValues> findUntypedData(
    902                 ArrayList<NamedContentValues> list, int type, String contentItemType) {
    903             ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
    904 
    905             // Loop through the ncv's, looking for an existing row
    906             for (NamedContentValues namedContentValues: list) {
    907                 Uri uri = namedContentValues.uri;
    908                 ContentValues cv = namedContentValues.values;
    909                 if (Data.CONTENT_URI.equals(uri)) {
    910                     String mimeType = cv.getAsString(Data.MIMETYPE);
    911                     if (mimeType.equals(contentItemType)) {
    912                         if (type != -1) {
    913                             int subtype = cv.getAsInteger(Phone.TYPE);
    914                             if (type != subtype) {
    915                                 continue;
    916                             }
    917                         }
    918                         result.add(namedContentValues);
    919                     }
    920                 }
    921             }
    922 
    923             // If we've found an existing data row, we'll delete it.  Any rows left at the
    924             // end should be deleted...
    925             for (NamedContentValues values : result) {
    926                 list.remove(values);
    927             }
    928 
    929             // Return the row found (or null)
    930             return result;
    931         }
    932 
    933         /**
    934          * Create a wrapper for a builder (insert or update) that also includes the NCV for
    935          * an existing row of this type.   If the SmartBuilder's cv field is not null, then
    936          * it represents the current (old) values of this field.  The caller can then check
    937          * whether the field is now different and needs to be updated; if it's not different,
    938          * the caller will simply return and not generate a new CPO.  Otherwise, the builder
    939          * should have its content values set, and the built CPO should be added to the
    940          * ContactOperations list.
    941          *
    942          * @param entity the contact entity (or null if this is a new contact)
    943          * @param mimeType the mime type of this row
    944          * @param type the subtype of this row
    945          * @param stringType for groups, the name of the group (type will be ignored), or null
    946          * @return the created SmartBuilder
    947          */
    948         public RowBuilder createBuilder(Entity entity, String mimeType, int type,
    949                 String stringType) {
    950             RowBuilder builder = null;
    951 
    952             if (entity != null) {
    953                 NamedContentValues ncv =
    954                     findTypedData(entity.getSubValues(), mimeType, type, stringType);
    955                 if (ncv != null) {
    956                     builder = new RowBuilder(
    957                             ContentProviderOperation
    958                                 .newUpdate(addCallerIsSyncAdapterParameter(
    959                                     dataUriFromNamedContentValues(ncv))),
    960                             ncv);
    961                 }
    962             }
    963 
    964             if (builder == null) {
    965                 builder = newRowBuilder(entity, mimeType);
    966             }
    967 
    968             // Return the appropriate builder (insert or update)
    969             // Caller will fill in the appropriate values; 4 MIMETYPE is already set
    970             return builder;
    971         }
    972 
    973         private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
    974             return createBuilder(entity, mimeType, type, null);
    975         }
    976 
    977         private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
    978             return createBuilder(entity, mimeType, -1, null);
    979         }
    980 
    981         private RowBuilder newRowBuilder(Entity entity, String mimeType) {
    982             // This is a new row; first get the contactId
    983             // If the Contact is new, use the saved back value; otherwise the value in the entity
    984             int contactId = mContactBackValue;
    985             if (entity != null) {
    986                 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
    987             }
    988 
    989             // Create an insert operation with the proper contactId reference
    990             RowBuilder builder =
    991                 new RowBuilder(ContentProviderOperation.newInsert(
    992                         addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
    993             if (entity == null) {
    994                 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
    995             } else {
    996                 builder.withValue(Data.RAW_CONTACT_ID, contactId);
    997             }
    998 
    999             // Set the mime type of the row
   1000             builder.withValue(Data.MIMETYPE, mimeType);
   1001             return builder;
   1002         }
   1003 
   1004         /**
   1005          * Compare a column in a ContentValues with an (old) value, and see if they are the
   1006          * same.  For this purpose, null and an empty string are considered the same.
   1007          * @param cv a ContentValues object, from a NamedContentValues
   1008          * @param column a column that might be in the ContentValues
   1009          * @param oldValue an old value (or null) to check against
   1010          * @return whether the column's value in the ContentValues matches oldValue
   1011          */
   1012         private static boolean cvCompareString(ContentValues cv, String column, String oldValue) {
   1013             if (cv.containsKey(column)) {
   1014                 if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
   1015                     return true;
   1016                 }
   1017             } else if (oldValue == null || oldValue.length() == 0) {
   1018                 return true;
   1019             }
   1020             return false;
   1021         }
   1022 
   1023         public void addChildren(Entity entity, ArrayList<String> children) {
   1024             RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
   1025             int i = 0;
   1026             for (String child: children) {
   1027                 builder.withValue(EasChildren.ROWS[i++], child);
   1028             }
   1029             add(builder.build());
   1030         }
   1031 
   1032         public void addGroup(Entity entity, String group) {
   1033             RowBuilder builder =
   1034                 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
   1035             builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
   1036             add(builder.build());
   1037         }
   1038 
   1039         public void addBirthday(Entity entity, String birthday) {
   1040             RowBuilder builder =
   1041                     typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
   1042             ContentValues cv = builder.cv;
   1043             if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
   1044                 return;
   1045             }
   1046             // TODO: Store the date in the format expected by EAS servers.
   1047             long millis = Utility.parseEmailDateTimeToMillis(birthday);
   1048             GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
   1049             cal.setTimeInMillis(millis);
   1050             if (cal.get(GregorianCalendar.HOUR_OF_DAY) >= 12) {
   1051                 cal.add(GregorianCalendar.DATE, 1);
   1052             }
   1053             String realBirthday = CalendarUtilities.calendarToBirthdayString(cal);
   1054             builder.withValue(Event.START_DATE, realBirthday);
   1055             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
   1056             add(builder.build());
   1057         }
   1058 
   1059         public void addName(Entity entity, String prefix, String givenName, String familyName,
   1060                 String middleName, String suffix, String yomiFirstName, String yomiLastName) {
   1061             RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
   1062             ContentValues cv = builder.cv;
   1063             if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
   1064                     cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
   1065                     cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
   1066                     cvCompareString(cv, StructuredName.PREFIX, prefix) &&
   1067                     cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
   1068                     cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
   1069                     cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
   1070                 return;
   1071             }
   1072             builder.withValue(StructuredName.GIVEN_NAME, givenName);
   1073             builder.withValue(StructuredName.FAMILY_NAME, familyName);
   1074             builder.withValue(StructuredName.MIDDLE_NAME, middleName);
   1075             builder.withValue(StructuredName.SUFFIX, suffix);
   1076             builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
   1077             builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
   1078             builder.withValue(StructuredName.PREFIX, prefix);
   1079             add(builder.build());
   1080         }
   1081 
   1082         public void addPersonal(Entity entity, EasPersonal personal) {
   1083             RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
   1084             ContentValues cv = builder.cv;
   1085             if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
   1086                     cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
   1087                 return;
   1088             }
   1089             if (!personal.hasData()) {
   1090                 return;
   1091             }
   1092             builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
   1093             builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
   1094             add(builder.build());
   1095         }
   1096 
   1097         public void addBusiness(Entity entity, EasBusiness business) {
   1098             RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
   1099             ContentValues cv = builder.cv;
   1100             if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
   1101                     cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
   1102                     cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
   1103                 return;
   1104             }
   1105             if (!business.hasData()) {
   1106                 return;
   1107             }
   1108             builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
   1109             builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
   1110             builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
   1111             add(builder.build());
   1112         }
   1113 
   1114         public void addPhoto(Entity entity, String photo) {
   1115             RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
   1116             // We're always going to add this; it's not worth trying to figure out whether the
   1117             // picture is the same as the one stored.
   1118             byte[] pic = Base64.decode(photo, Base64.DEFAULT);
   1119             builder.withValue(Photo.PHOTO, pic);
   1120             add(builder.build());
   1121         }
   1122 
   1123         public void addPhone(Entity entity, int type, String phone) {
   1124             RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
   1125             ContentValues cv = builder.cv;
   1126             if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
   1127                 return;
   1128             }
   1129             builder.withValue(Phone.TYPE, type);
   1130             builder.withValue(Phone.NUMBER, phone);
   1131             add(builder.build());
   1132         }
   1133 
   1134         public void addWebpage(Entity entity, String url) {
   1135             RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
   1136             ContentValues cv = builder.cv;
   1137             if (cv != null && cvCompareString(cv, Website.URL, url)) {
   1138                 return;
   1139             }
   1140             builder.withValue(Website.TYPE, Website.TYPE_WORK);
   1141             builder.withValue(Website.URL, url);
   1142             add(builder.build());
   1143         }
   1144 
   1145         public void addRelation(Entity entity, int type, String value) {
   1146             RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
   1147             ContentValues cv = builder.cv;
   1148             if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
   1149                 return;
   1150             }
   1151             builder.withValue(Relation.TYPE, type);
   1152             builder.withValue(Relation.DATA, value);
   1153             add(builder.build());
   1154         }
   1155 
   1156         public void addNickname(Entity entity, String name) {
   1157             RowBuilder builder =
   1158                 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
   1159             ContentValues cv = builder.cv;
   1160             if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
   1161                 return;
   1162             }
   1163             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
   1164             builder.withValue(Nickname.NAME, name);
   1165             add(builder.build());
   1166         }
   1167 
   1168         public void addPostal(Entity entity, int type, String street, String city, String state,
   1169                 String country, String code) {
   1170             RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
   1171                     type);
   1172             ContentValues cv = builder.cv;
   1173             if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
   1174                     cvCompareString(cv, StructuredPostal.STREET, street) &&
   1175                     cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
   1176                     cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
   1177                     cvCompareString(cv, StructuredPostal.REGION, state)) {
   1178                 return;
   1179             }
   1180             builder.withValue(StructuredPostal.TYPE, type);
   1181             builder.withValue(StructuredPostal.CITY, city);
   1182             builder.withValue(StructuredPostal.STREET, street);
   1183             builder.withValue(StructuredPostal.COUNTRY, country);
   1184             builder.withValue(StructuredPostal.POSTCODE, code);
   1185             builder.withValue(StructuredPostal.REGION, state);
   1186             add(builder.build());
   1187         }
   1188 
   1189        /**
   1190          * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
   1191          * match them with existing rows; if there's a match, everything's great.  Otherwise, we
   1192          * either need to add a new row for the data, or we have to replace an existing one
   1193          * that no longer matches.  This is similar to the way Emails are handled.
   1194          */
   1195         public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
   1196                 int type, int maxRows) {
   1197             // Make a list of all same type rows in the existing entity
   1198             ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
   1199             ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
   1200             if (entity != null) {
   1201                 oldValues = findUntypedData(entityValues, type, mimeType);
   1202                 entityValues = entity.getSubValues();
   1203             }
   1204 
   1205             // These will be rows needing replacement with new values
   1206             ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
   1207 
   1208             // The count of existing rows
   1209             int numRows = oldValues.size();
   1210             for (UntypedRow row: rows) {
   1211                 boolean found = false;
   1212                 // If we already have this row, mark it
   1213                 for (NamedContentValues ncv: oldValues) {
   1214                     ContentValues cv = ncv.values;
   1215                     String data = cv.getAsString(COMMON_DATA_ROW);
   1216                     int rowType = -1;
   1217                     if (cv.containsKey(COMMON_TYPE_ROW)) {
   1218                         rowType = cv.getAsInteger(COMMON_TYPE_ROW);
   1219                     }
   1220                     if (row.isSameAs(rowType, data)) {
   1221                         cv.put(FOUND_DATA_ROW, true);
   1222                         // Remove this to indicate it's still being used
   1223                         entityValues.remove(ncv);
   1224                         found = true;
   1225                         break;
   1226                     }
   1227                 }
   1228                 if (!found) {
   1229                     // If we don't, there are two possibilities
   1230                     if (numRows < maxRows) {
   1231                         // If there are available rows, add a new one
   1232                         RowBuilder builder = newRowBuilder(entity, mimeType);
   1233                         row.addValues(builder);
   1234                         add(builder.build());
   1235                         numRows++;
   1236                     } else {
   1237                         // Otherwise, say we need to replace a row with this
   1238                         rowsToReplace.add(row);
   1239                     }
   1240                 }
   1241             }
   1242 
   1243             // Go through rows needing replacement
   1244             for (UntypedRow row: rowsToReplace) {
   1245                 for (NamedContentValues ncv: oldValues) {
   1246                     ContentValues cv = ncv.values;
   1247                     // Find a row that hasn't been used (i.e. doesn't match current rows)
   1248                     if (!cv.containsKey(FOUND_DATA_ROW)) {
   1249                         // And update it
   1250                         RowBuilder builder = new RowBuilder(
   1251                                 ContentProviderOperation
   1252                                     .newUpdate(addCallerIsSyncAdapterParameter(
   1253                                         dataUriFromNamedContentValues(ncv))),
   1254                                 ncv);
   1255                         row.addValues(builder);
   1256                         add(builder.build());
   1257                     }
   1258                 }
   1259             }
   1260         }
   1261 
   1262         public void addOrganization(Entity entity, int type, String company, String title,
   1263                 String department, String yomiCompanyName, String officeLocation) {
   1264             RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
   1265             ContentValues cv = builder.cv;
   1266             if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
   1267                     cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
   1268                     cvCompareString(cv, Organization.DEPARTMENT, department) &&
   1269                     cvCompareString(cv, Organization.TITLE, title) &&
   1270                     cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
   1271                 return;
   1272             }
   1273             builder.withValue(Organization.TYPE, type);
   1274             builder.withValue(Organization.COMPANY, company);
   1275             builder.withValue(Organization.TITLE, title);
   1276             builder.withValue(Organization.DEPARTMENT, department);
   1277             builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
   1278             builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
   1279             add(builder.build());
   1280         }
   1281 
   1282         public void addNote(Entity entity, String note) {
   1283             RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
   1284             ContentValues cv = builder.cv;
   1285             if (note == null) return;
   1286             note = note.replaceAll("\r\n", "\n");
   1287             if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
   1288                 return;
   1289             }
   1290 
   1291             // Reject notes with nothing in them.  Often, we get something from Outlook when
   1292             // nothing was ever entered.  Sigh.
   1293             int len = note.length();
   1294             int i = 0;
   1295             for (; i < len; i++) {
   1296                 char c = note.charAt(i);
   1297                 if (!Character.isWhitespace(c)) {
   1298                     break;
   1299                 }
   1300             }
   1301             if (i == len) return;
   1302 
   1303             builder.withValue(Note.NOTE, note);
   1304             add(builder.build());
   1305         }
   1306     }
   1307 
   1308     @Override
   1309     protected void wipe() {
   1310         LogUtils.w(TAG, "Wiping contacts for account %d", mAccount.mId);
   1311         EasSyncContacts.wipeAccountFromContentProvider(mContext,
   1312                 mAccount.mEmailAddress);
   1313     }
   1314 }
   1315