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