Home | History | Annotate | Download | only in adapter
      1 /*
      2  * Copyright (C) 2008-2009 Marc Blank
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.exchange.adapter;
     19 
     20 import com.android.email.provider.EmailContent.Mailbox;
     21 import com.android.exchange.Eas;
     22 import com.android.exchange.EasSyncService;
     23 
     24 import android.content.ContentProviderClient;
     25 import android.content.ContentProviderOperation;
     26 import android.content.ContentProviderResult;
     27 import android.content.ContentResolver;
     28 import android.content.ContentUris;
     29 import android.content.ContentValues;
     30 import android.content.Entity;
     31 import android.content.EntityIterator;
     32 import android.content.OperationApplicationException;
     33 import android.content.ContentProviderOperation.Builder;
     34 import android.content.Entity.NamedContentValues;
     35 import android.database.Cursor;
     36 import android.net.Uri;
     37 import android.os.RemoteException;
     38 import android.provider.ContactsContract;
     39 import android.provider.SyncStateContract;
     40 import android.provider.ContactsContract.Data;
     41 import android.provider.ContactsContract.Groups;
     42 import android.provider.ContactsContract.RawContacts;
     43 import android.provider.ContactsContract.RawContactsEntity;
     44 import android.provider.ContactsContract.Settings;
     45 import android.provider.ContactsContract.SyncState;
     46 import android.provider.ContactsContract.CommonDataKinds.Email;
     47 import android.provider.ContactsContract.CommonDataKinds.Event;
     48 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     49 import android.provider.ContactsContract.CommonDataKinds.Im;
     50 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     51 import android.provider.ContactsContract.CommonDataKinds.Note;
     52 import android.provider.ContactsContract.CommonDataKinds.Organization;
     53 import android.provider.ContactsContract.CommonDataKinds.Phone;
     54 import android.provider.ContactsContract.CommonDataKinds.Photo;
     55 import android.provider.ContactsContract.CommonDataKinds.Relation;
     56 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     57 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     58 import android.provider.ContactsContract.CommonDataKinds.Website;
     59 import android.text.util.Rfc822Token;
     60 import android.text.util.Rfc822Tokenizer;
     61 import android.util.Base64;
     62 import android.util.Log;
     63 
     64 import java.io.IOException;
     65 import java.io.InputStream;
     66 import java.util.ArrayList;
     67 
     68 /**
     69  * Sync adapter for EAS Contacts
     70  *
     71  */
     72 public class ContactsSyncAdapter extends AbstractSyncAdapter {
     73 
     74     private static final String TAG = "EasContactsSyncAdapter";
     75     private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
     76     private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
     77     private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
     78     private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
     79 
     80     private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
     81         = new ArrayList<NamedContentValues>();
     82 
     83     private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
     84 
     85     private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
     86         Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
     87         Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
     88         Tags.CONTACTS_HOME_ADDRESS_STATE,
     89         Tags.CONTACTS_HOME_ADDRESS_STREET};
     90 
     91     private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
     92         Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
     93         Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
     94         Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
     95         Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
     96 
     97     private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
     98         Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
     99         Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
    100         Tags.CONTACTS_OTHER_ADDRESS_STATE,
    101         Tags.CONTACTS_OTHER_ADDRESS_STREET};
    102 
    103     private static final int MAX_IM_ROWS = 3;
    104     private static final int MAX_EMAIL_ROWS = 3;
    105     private static final int MAX_PHONE_ROWS = 2;
    106     private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
    107     private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
    108 
    109     private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
    110         Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
    111 
    112     private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
    113         Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
    114 
    115     private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
    116         Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
    117 
    118     private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
    119         Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
    120 
    121     private static final Object sSyncKeyLock = new Object();
    122 
    123     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
    124     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
    125 
    126     private boolean mGroupsUsed = false;
    127 
    128     public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
    129         super(mailbox, service);
    130     }
    131 
    132     static Uri addCallerIsSyncAdapterParameter(Uri uri) {
    133         return uri.buildUpon()
    134                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    135                 .build();
    136     }
    137 
    138     @Override
    139     public boolean isSyncable() {
    140         return ContentResolver.getSyncAutomatically(
    141                 mAccountManagerAccount, ContactsContract.AUTHORITY);
    142     }
    143 
    144     @Override
    145     public boolean parse(InputStream is) throws IOException {
    146         EasContactsSyncParser p = new EasContactsSyncParser(is, this);
    147         return p.parse();
    148     }
    149 
    150     interface UntypedRow {
    151         public void addValues(RowBuilder builder);
    152         public boolean isSameAs(int type, String value);
    153     }
    154 
    155     /**
    156      * We get our SyncKey from ContactsProvider.  If there's not one, we set it to "0" (the reset
    157      * state) and save that away.
    158      */
    159     @Override
    160     public String getSyncKey() throws IOException {
    161         synchronized (sSyncKeyLock) {
    162             ContentProviderClient client = mService.mContentResolver
    163                     .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    164             try {
    165                 byte[] data = SyncStateContract.Helpers.get(client,
    166                         ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount);
    167                 if (data == null || data.length == 0) {
    168                     // Initialize the SyncKey
    169                     setSyncKey("0", false);
    170                     // Make sure ungrouped contacts for Exchange are defaultly visible
    171                     ContentValues cv = new ContentValues();
    172                     cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
    173                     cv.put(Groups.ACCOUNT_TYPE,
    174                             com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
    175                     cv.put(Settings.UNGROUPED_VISIBLE, true);
    176                     client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
    177                     return "0";
    178                 } else {
    179                     return new String(data);
    180                 }
    181             } catch (RemoteException e) {
    182                 throw new IOException("Can't get SyncKey from ContactsProvider");
    183             }
    184         }
    185     }
    186 
    187     /**
    188      * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
    189      * cases, the SyncKey is set within ContactOperations
    190      */
    191     @Override
    192     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
    193         synchronized (sSyncKeyLock) {
    194             if ("0".equals(syncKey) || !inCommands) {
    195                 ContentProviderClient client = mService.mContentResolver
    196                         .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    197                 try {
    198                     SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
    199                             mAccountManagerAccount, syncKey.getBytes());
    200                     userLog("SyncKey set to ", syncKey, " in ContactsProvider");
    201                 } catch (RemoteException e) {
    202                     throw new IOException("Can't set SyncKey in ContactsProvider");
    203                 }
    204             }
    205             mMailbox.mSyncKey = syncKey;
    206         }
    207     }
    208 
    209     public static final class EasChildren {
    210         private EasChildren() {}
    211 
    212         /** MIME type used when storing this in data table. */
    213         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
    214         public static final int MAX_CHILDREN = 8;
    215         public static final String[] ROWS =
    216             new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
    217     }
    218 
    219     public static final class EasPersonal {
    220         String anniversary;
    221         String fileAs;
    222 
    223             /** MIME type used when storing this in data table. */
    224         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
    225         public static final String ANNIVERSARY = "data2";
    226         public static final String FILE_AS = "data4";
    227 
    228         boolean hasData() {
    229             return anniversary != null || fileAs != null;
    230         }
    231     }
    232 
    233     public static final class EasBusiness {
    234         String customerId;
    235         String governmentId;
    236         String accountName;
    237 
    238         /** MIME type used when storing this in data table. */
    239         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
    240         public static final String CUSTOMER_ID = "data6";
    241         public static final String GOVERNMENT_ID = "data7";
    242         public static final String ACCOUNT_NAME = "data8";
    243 
    244         boolean hasData() {
    245             return customerId != null || governmentId != null || accountName != null;
    246         }
    247     }
    248 
    249     public static final class Address {
    250         String city;
    251         String country;
    252         String code;
    253         String street;
    254         String state;
    255 
    256         boolean hasData() {
    257             return city != null || country != null || code != null || state != null
    258                 || street != null;
    259         }
    260     }
    261 
    262     class EmailRow implements UntypedRow {
    263         String email;
    264         String displayName;
    265 
    266         public EmailRow(String _email) {
    267             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
    268             // Can't happen, but belt & suspenders
    269             if (tokens.length == 0) {
    270                 email = "";
    271                 displayName = "";
    272             } else {
    273                 Rfc822Token token = tokens[0];
    274                 email = token.getAddress();
    275                 displayName = token.getName();
    276             }
    277         }
    278 
    279         public void addValues(RowBuilder builder) {
    280             builder.withValue(Email.DATA, email);
    281             builder.withValue(Email.DISPLAY_NAME, displayName);
    282         }
    283 
    284         public boolean isSameAs(int type, String value) {
    285             return email.equalsIgnoreCase(value);
    286         }
    287     }
    288 
    289     class ImRow implements UntypedRow {
    290         String im;
    291 
    292         public ImRow(String _im) {
    293             im = _im;
    294         }
    295 
    296         public void addValues(RowBuilder builder) {
    297             builder.withValue(Im.DATA, im);
    298         }
    299 
    300         public boolean isSameAs(int type, String value) {
    301             return im.equalsIgnoreCase(value);
    302         }
    303     }
    304 
    305     class PhoneRow implements UntypedRow {
    306         String phone;
    307         int type;
    308 
    309         public PhoneRow(String _phone, int _type) {
    310             phone = _phone;
    311             type = _type;
    312         }
    313 
    314         public void addValues(RowBuilder builder) {
    315             builder.withValue(Im.DATA, phone);
    316             builder.withValue(Phone.TYPE, type);
    317         }
    318 
    319         public boolean isSameAs(int _type, String value) {
    320             return type == _type && phone.equalsIgnoreCase(value);
    321         }
    322     }
    323 
    324    class EasContactsSyncParser extends AbstractSyncParser {
    325 
    326         String[] mBindArgument = new String[1];
    327         String mMailboxIdAsString;
    328         Uri mAccountUri;
    329         ContactOperations ops = new ContactOperations();
    330 
    331         public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
    332                 throws IOException {
    333             super(in, adapter);
    334             mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
    335         }
    336 
    337         @Override
    338         public void wipe() {
    339             mContentResolver.delete(mAccountUri, null, null);
    340         }
    341 
    342         public void addData(String serverId, ContactOperations ops, Entity entity)
    343                 throws IOException {
    344             String fileAs = null;
    345             String prefix = null;
    346             String firstName = null;
    347             String lastName = null;
    348             String middleName = null;
    349             String suffix = null;
    350             String companyName = null;
    351             String yomiFirstName = null;
    352             String yomiLastName = null;
    353             String yomiCompanyName = null;
    354             String title = null;
    355             String department = null;
    356             String officeLocation = null;
    357             Address home = new Address();
    358             Address work = new Address();
    359             Address other = new Address();
    360             EasBusiness business = new EasBusiness();
    361             EasPersonal personal = new EasPersonal();
    362             ArrayList<String> children = new ArrayList<String>();
    363             ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
    364             ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
    365             ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
    366             ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
    367             if (entity == null) {
    368                 ops.newContact(serverId);
    369             }
    370 
    371             while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
    372                 switch (tag) {
    373                     case Tags.CONTACTS_FIRST_NAME:
    374                         firstName = getValue();
    375                         break;
    376                     case Tags.CONTACTS_LAST_NAME:
    377                         lastName = getValue();
    378                         break;
    379                     case Tags.CONTACTS_MIDDLE_NAME:
    380                         middleName = getValue();
    381                         break;
    382                     case Tags.CONTACTS_FILE_AS:
    383                         fileAs = getValue();
    384                         break;
    385                     case Tags.CONTACTS_SUFFIX:
    386                         suffix = getValue();
    387                         break;
    388                     case Tags.CONTACTS_COMPANY_NAME:
    389                         companyName = getValue();
    390                         break;
    391                     case Tags.CONTACTS_JOB_TITLE:
    392                         title = getValue();
    393                         break;
    394                     case Tags.CONTACTS_EMAIL1_ADDRESS:
    395                     case Tags.CONTACTS_EMAIL2_ADDRESS:
    396                     case Tags.CONTACTS_EMAIL3_ADDRESS:
    397                         emails.add(new EmailRow(getValue()));
    398                         break;
    399                     case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
    400                     case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
    401                         workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
    402                         break;
    403                     case Tags.CONTACTS2_MMS:
    404                         ops.addPhone(entity, Phone.TYPE_MMS, getValue());
    405                         break;
    406                     case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
    407                         ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
    408                         break;
    409                     case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
    410                         ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
    411                         break;
    412                     case Tags.CONTACTS_HOME_FAX_NUMBER:
    413                         ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
    414                         break;
    415                     case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
    416                     case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
    417                         homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
    418                         break;
    419                     case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
    420                         ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
    421                         break;
    422                     case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
    423                         ops.addPhone(entity, Phone.TYPE_CAR, getValue());
    424                         break;
    425                     case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
    426                         ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
    427                         break;
    428                     case Tags.CONTACTS_PAGER_NUMBER:
    429                         ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
    430                         break;
    431                     case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
    432                         ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
    433                         break;
    434                     case Tags.CONTACTS2_IM_ADDRESS:
    435                     case Tags.CONTACTS2_IM_ADDRESS_2:
    436                     case Tags.CONTACTS2_IM_ADDRESS_3:
    437                         ims.add(new ImRow(getValue()));
    438                         break;
    439                     case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
    440                         work.city = getValue();
    441                         break;
    442                     case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
    443                         work.country = getValue();
    444                         break;
    445                     case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
    446                         work.code = getValue();
    447                         break;
    448                     case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
    449                         work.state = getValue();
    450                         break;
    451                     case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
    452                         work.street = getValue();
    453                         break;
    454                     case Tags.CONTACTS_HOME_ADDRESS_CITY:
    455                         home.city = getValue();
    456                         break;
    457                     case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
    458                         home.country = getValue();
    459                         break;
    460                     case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
    461                         home.code = getValue();
    462                         break;
    463                     case Tags.CONTACTS_HOME_ADDRESS_STATE:
    464                         home.state = getValue();
    465                         break;
    466                     case Tags.CONTACTS_HOME_ADDRESS_STREET:
    467                         home.street = getValue();
    468                         break;
    469                     case Tags.CONTACTS_OTHER_ADDRESS_CITY:
    470                         other.city = getValue();
    471                         break;
    472                     case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
    473                         other.country = getValue();
    474                         break;
    475                     case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
    476                         other.code = getValue();
    477                         break;
    478                     case Tags.CONTACTS_OTHER_ADDRESS_STATE:
    479                         other.state = getValue();
    480                         break;
    481                     case Tags.CONTACTS_OTHER_ADDRESS_STREET:
    482                         other.street = getValue();
    483                         break;
    484 
    485                     case Tags.CONTACTS_CHILDREN:
    486                         childrenParser(children);
    487                         break;
    488 
    489                     case Tags.CONTACTS_YOMI_COMPANY_NAME:
    490                         yomiCompanyName = getValue();
    491                         break;
    492                     case Tags.CONTACTS_YOMI_FIRST_NAME:
    493                         yomiFirstName = getValue();
    494                         break;
    495                     case Tags.CONTACTS_YOMI_LAST_NAME:
    496                         yomiLastName = getValue();
    497                         break;
    498 
    499                     case Tags.CONTACTS2_NICKNAME:
    500                         ops.addNickname(entity, getValue());
    501                         break;
    502 
    503                     case Tags.CONTACTS_ASSISTANT_NAME:
    504                         ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
    505                         break;
    506                     case Tags.CONTACTS2_MANAGER_NAME:
    507                         ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
    508                         break;
    509                     case Tags.CONTACTS_SPOUSE:
    510                         ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
    511                         break;
    512                     case Tags.CONTACTS_DEPARTMENT:
    513                         department = getValue();
    514                         break;
    515                     case Tags.CONTACTS_TITLE:
    516                         prefix = getValue();
    517                         break;
    518 
    519                     // EAS Business
    520                     case Tags.CONTACTS_OFFICE_LOCATION:
    521                         officeLocation = getValue();
    522                         break;
    523                     case Tags.CONTACTS2_CUSTOMER_ID:
    524                         business.customerId = getValue();
    525                         break;
    526                     case Tags.CONTACTS2_GOVERNMENT_ID:
    527                         business.governmentId = getValue();
    528                         break;
    529                     case Tags.CONTACTS2_ACCOUNT_NAME:
    530                         business.accountName = getValue();
    531                         break;
    532 
    533                     // EAS Personal
    534                     case Tags.CONTACTS_ANNIVERSARY:
    535                         personal.anniversary = getValue();
    536                         break;
    537                     case Tags.CONTACTS_BIRTHDAY:
    538                         ops.addBirthday(entity, getValue());
    539                         break;
    540                     case Tags.CONTACTS_WEBPAGE:
    541                         ops.addWebpage(entity, getValue());
    542                         break;
    543 
    544                     case Tags.CONTACTS_PICTURE:
    545                         ops.addPhoto(entity, getValue());
    546                         break;
    547 
    548                     case Tags.BASE_BODY:
    549                         ops.addNote(entity, bodyParser());
    550                         break;
    551                     case Tags.CONTACTS_BODY:
    552                         ops.addNote(entity, getValue());
    553                         break;
    554 
    555                     case Tags.CONTACTS_CATEGORIES:
    556                         mGroupsUsed = true;
    557                         categoriesParser(ops, entity);
    558                         break;
    559 
    560                     case Tags.CONTACTS_COMPRESSED_RTF:
    561                         // We don't use this, and it isn't necessary to upload, so we'll ignore it
    562                         skipTag();
    563                         break;
    564 
    565                     default:
    566                         skipTag();
    567                 }
    568             }
    569 
    570             // We must have first name, last name, or company name
    571             String name = null;
    572             if (firstName != null || lastName != null) {
    573                 if (firstName == null) {
    574                     name = lastName;
    575                 } else if (lastName == null) {
    576                     name = firstName;
    577                 } else {
    578                     name = firstName + ' ' + lastName;
    579                 }
    580             } else if (companyName != null) {
    581                 name = companyName;
    582             }
    583 
    584             ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
    585                     yomiFirstName, yomiLastName, fileAs);
    586             ops.addBusiness(entity, business);
    587             ops.addPersonal(entity, personal);
    588 
    589             ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
    590             ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
    591             ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
    592                     MAX_PHONE_ROWS);
    593             ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
    594                     MAX_PHONE_ROWS);
    595 
    596             if (!children.isEmpty()) {
    597                 ops.addChildren(entity, children);
    598             }
    599 
    600             if (work.hasData()) {
    601                 ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
    602                         work.state, work.country, work.code);
    603             }
    604             if (home.hasData()) {
    605                 ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
    606                         home.state, home.country, home.code);
    607             }
    608             if (other.hasData()) {
    609                 ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
    610                         other.state, other.country, other.code);
    611             }
    612 
    613             if (companyName != null) {
    614                 ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
    615                         yomiCompanyName, officeLocation);
    616             }
    617 
    618             if (entity != null) {
    619                 // We've been removing rows from the list as they've been found in the xml
    620                 // Any that are left must have been deleted on the server
    621                 ArrayList<NamedContentValues> ncvList = entity.getSubValues();
    622                 for (NamedContentValues ncv: ncvList) {
    623                     // These rows need to be deleted...
    624                     Uri u = dataUriFromNamedContentValues(ncv);
    625                     ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
    626                             .build());
    627                 }
    628             }
    629         }
    630 
    631         private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
    632             while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
    633                 switch (tag) {
    634                     case Tags.CONTACTS_CATEGORY:
    635                         ops.addGroup(entity, getValue());
    636                         break;
    637                     default:
    638                         skipTag();
    639                 }
    640             }
    641         }
    642 
    643         private void childrenParser(ArrayList<String> children) throws IOException {
    644             while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
    645                 switch (tag) {
    646                     case Tags.CONTACTS_CHILD:
    647                         if (children.size() < EasChildren.MAX_CHILDREN) {
    648                             children.add(getValue());
    649                         }
    650                         break;
    651                     default:
    652                         skipTag();
    653                 }
    654             }
    655         }
    656 
    657         private String bodyParser() throws IOException {
    658             String body = null;
    659             while (nextTag(Tags.BASE_BODY) != END) {
    660                 switch (tag) {
    661                     case Tags.BASE_DATA:
    662                         body = getValue();
    663                         break;
    664                     default:
    665                         skipTag();
    666                 }
    667             }
    668             return body;
    669         }
    670 
    671         public void addParser(ContactOperations ops) throws IOException {
    672             String serverId = null;
    673             while (nextTag(Tags.SYNC_ADD) != END) {
    674                 switch (tag) {
    675                     case Tags.SYNC_SERVER_ID: // same as
    676                         serverId = getValue();
    677                         break;
    678                     case Tags.SYNC_APPLICATION_DATA:
    679                         addData(serverId, ops, null);
    680                         break;
    681                     default:
    682                         skipTag();
    683                 }
    684             }
    685         }
    686 
    687         private Cursor getServerIdCursor(String serverId) {
    688             mBindArgument[0] = serverId;
    689             return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
    690                     mBindArgument, null);
    691         }
    692 
    693         private Cursor getClientIdCursor(String clientId) {
    694             mBindArgument[0] = clientId;
    695             return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
    696                     mBindArgument, null);
    697         }
    698 
    699         public void deleteParser(ContactOperations ops) throws IOException {
    700             while (nextTag(Tags.SYNC_DELETE) != END) {
    701                 switch (tag) {
    702                     case Tags.SYNC_SERVER_ID:
    703                         String serverId = getValue();
    704                         // Find the message in this mailbox with the given serverId
    705                         Cursor c = getServerIdCursor(serverId);
    706                         try {
    707                             if (c.moveToFirst()) {
    708                                 userLog("Deleting ", serverId);
    709                                 ops.delete(c.getLong(0));
    710                             }
    711                         } finally {
    712                             c.close();
    713                         }
    714                         break;
    715                     default:
    716                         skipTag();
    717                 }
    718             }
    719         }
    720 
    721         class ServerChange {
    722             long id;
    723             boolean read;
    724 
    725             ServerChange(long _id, boolean _read) {
    726                 id = _id;
    727                 read = _read;
    728             }
    729         }
    730 
    731         /**
    732          * Changes are handled row by row, and only changed/new rows are acted upon
    733          * @param ops the array of pending ContactProviderOperations.
    734          * @throws IOException
    735          */
    736         public void changeParser(ContactOperations ops) throws IOException {
    737             String serverId = null;
    738             Entity entity = null;
    739             while (nextTag(Tags.SYNC_CHANGE) != END) {
    740                 switch (tag) {
    741                     case Tags.SYNC_SERVER_ID:
    742                         serverId = getValue();
    743                         Cursor c = getServerIdCursor(serverId);
    744                         try {
    745                             if (c.moveToFirst()) {
    746                                 // TODO Handle deleted individual rows...
    747                                 Uri uri = ContentUris.withAppendedId(
    748                                         RawContacts.CONTENT_URI, c.getLong(0));
    749                                 uri = Uri.withAppendedPath(
    750                                         uri, RawContacts.Entity.CONTENT_DIRECTORY);
    751                                 EntityIterator entityIterator = RawContacts.newEntityIterator(
    752                                     mContentResolver.query(uri, null, null, null, null));
    753                                 if (entityIterator.hasNext()) {
    754                                     entity = entityIterator.next();
    755                                 }
    756                                 userLog("Changing contact ", serverId);
    757                             }
    758                         } finally {
    759                             c.close();
    760                         }
    761                         break;
    762                     case Tags.SYNC_APPLICATION_DATA:
    763                         addData(serverId, ops, entity);
    764                         break;
    765                     default:
    766                         skipTag();
    767                 }
    768             }
    769         }
    770 
    771         @Override
    772         public void commandsParser() throws IOException {
    773             while (nextTag(Tags.SYNC_COMMANDS) != END) {
    774                 if (tag == Tags.SYNC_ADD) {
    775                     addParser(ops);
    776                     incrementChangeCount();
    777                 } else if (tag == Tags.SYNC_DELETE) {
    778                     deleteParser(ops);
    779                     incrementChangeCount();
    780                 } else if (tag == Tags.SYNC_CHANGE) {
    781                     changeParser(ops);
    782                     incrementChangeCount();
    783                 } else
    784                     skipTag();
    785             }
    786         }
    787 
    788         @Override
    789         public void commit() throws IOException {
    790            // Save the syncKey here, using the Helper provider by Contacts provider
    791             userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
    792             ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
    793                     mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
    794 
    795             // Execute these all at once...
    796             ops.execute();
    797 
    798             if (ops.mResults != null) {
    799                 ContentValues cv = new ContentValues();
    800                 cv.put(RawContacts.DIRTY, 0);
    801                 for (int i = 0; i < ops.mContactIndexCount; i++) {
    802                     int index = ops.mContactIndexArray[i];
    803                     Uri u = ops.mResults[index].uri;
    804                     if (u != null) {
    805                         String idString = u.getLastPathSegment();
    806                         mContentResolver.update(
    807                                 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
    808                                 RawContacts._ID + "=" + idString, null);
    809                     }
    810                 }
    811             }
    812         }
    813 
    814         public void addResponsesParser() throws IOException {
    815             String serverId = null;
    816             String clientId = null;
    817             ContentValues cv = new ContentValues();
    818             while (nextTag(Tags.SYNC_ADD) != END) {
    819                 switch (tag) {
    820                     case Tags.SYNC_SERVER_ID:
    821                         serverId = getValue();
    822                         break;
    823                     case Tags.SYNC_CLIENT_ID:
    824                         clientId = getValue();
    825                         break;
    826                     case Tags.SYNC_STATUS:
    827                         getValue();
    828                         break;
    829                     default:
    830                         skipTag();
    831                 }
    832             }
    833 
    834             // This is theoretically impossible, but...
    835             if (clientId == null || serverId == null) return;
    836 
    837             Cursor c = getClientIdCursor(clientId);
    838             try {
    839                 if (c.moveToFirst()) {
    840                     cv.put(RawContacts.SOURCE_ID, serverId);
    841                     cv.put(RawContacts.DIRTY, 0);
    842                     ops.add(ContentProviderOperation.newUpdate(
    843                             ContentUris.withAppendedId(
    844                                     addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
    845                                     c.getLong(0)))
    846                             .withValues(cv)
    847                             .build());
    848                     userLog("New contact " + clientId + " was given serverId: " + serverId);
    849                 }
    850             } finally {
    851                 c.close();
    852             }
    853         }
    854 
    855         public void changeResponsesParser() throws IOException {
    856             String serverId = null;
    857             String status = null;
    858             while (nextTag(Tags.SYNC_CHANGE) != END) {
    859                 switch (tag) {
    860                     case Tags.SYNC_SERVER_ID:
    861                         serverId = getValue();
    862                         break;
    863                     case Tags.SYNC_STATUS:
    864                         status = getValue();
    865                         break;
    866                     default:
    867                         skipTag();
    868                 }
    869             }
    870             if (serverId != null && status != null) {
    871                 userLog("Changed contact " + serverId + " failed with status: " + status);
    872             }
    873         }
    874 
    875 
    876         @Override
    877         public void responsesParser() throws IOException {
    878             // Handle server responses here (for Add and Change)
    879             while (nextTag(Tags.SYNC_RESPONSES) != END) {
    880                 if (tag == Tags.SYNC_ADD) {
    881                     addResponsesParser();
    882                 } else if (tag == Tags.SYNC_CHANGE) {
    883                     changeResponsesParser();
    884                 } else
    885                     skipTag();
    886             }
    887         }
    888     }
    889 
    890 
    891     private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
    892         return uri.buildUpon()
    893             .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
    894             .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
    895                     com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
    896             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    897             .build();
    898     }
    899 
    900     /**
    901      * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
    902      * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
    903      * represent the current values of that row, that can be compared against current values to
    904      * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
    905      * the Builder.
    906      */
    907     private class RowBuilder {
    908         Builder builder;
    909         ContentValues cv;
    910 
    911         public RowBuilder(Builder _builder) {
    912             builder = _builder;
    913         }
    914 
    915         public RowBuilder(Builder _builder, NamedContentValues _ncv) {
    916             builder = _builder;
    917             cv = _ncv.values;
    918         }
    919 
    920         RowBuilder withValues(ContentValues values) {
    921             builder.withValues(values);
    922             return this;
    923         }
    924 
    925         RowBuilder withValueBackReference(String key, int previousResult) {
    926             builder.withValueBackReference(key, previousResult);
    927             return this;
    928         }
    929 
    930         ContentProviderOperation build() {
    931             return builder.build();
    932         }
    933 
    934         RowBuilder withValue(String key, Object value) {
    935             builder.withValue(key, value);
    936             return this;
    937         }
    938     }
    939 
    940     private class ContactOperations extends ArrayList<ContentProviderOperation> {
    941         private static final long serialVersionUID = 1L;
    942         private int mCount = 0;
    943         private int mContactBackValue = mCount;
    944         // Make an array big enough for the PIM window (max items we can get)
    945         private int[] mContactIndexArray =
    946             new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)];
    947         private int mContactIndexCount = 0;
    948         private ContentProviderResult[] mResults = null;
    949 
    950         @Override
    951         public boolean add(ContentProviderOperation op) {
    952             super.add(op);
    953             mCount++;
    954             return true;
    955         }
    956 
    957         public void newContact(String serverId) {
    958             Builder builder = ContentProviderOperation
    959                 .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
    960             ContentValues values = new ContentValues();
    961             values.put(RawContacts.SOURCE_ID, serverId);
    962             builder.withValues(values);
    963             mContactBackValue = mCount;
    964             mContactIndexArray[mContactIndexCount++] = mCount;
    965             add(builder.build());
    966         }
    967 
    968         public void delete(long id) {
    969             add(ContentProviderOperation
    970                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
    971                             .buildUpon()
    972                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
    973                             .build())
    974                     .build());
    975         }
    976 
    977         public void execute() {
    978             synchronized (mService.getSynchronizer()) {
    979                 if (!mService.isStopped()) {
    980                     try {
    981                         if (!isEmpty()) {
    982                             mService.userLog("Executing ", size(), " CPO's");
    983                             mResults = mContext.getContentResolver().applyBatch(
    984                                     ContactsContract.AUTHORITY, this);
    985                         }
    986                     } catch (RemoteException e) {
    987                         // There is nothing sensible to be done here
    988                         Log.e(TAG, "problem inserting contact during server update", e);
    989                     } catch (OperationApplicationException e) {
    990                         // There is nothing sensible to be done here
    991                         Log.e(TAG, "problem inserting contact during server update", e);
    992                     }
    993                 }
    994             }
    995         }
    996 
    997         /**
    998          * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
    999          * tries to find a match, returning it
   1000          * @param list the list of NCV's from the contact entity
   1001          * @param contentItemType the mime type we're looking for
   1002          * @param type the subtype (e.g. HOME, WORK, etc.)
   1003          * @return the matching NCV or null if not found
   1004          */
   1005         private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
   1006                 String contentItemType, int type, String stringType) {
   1007             NamedContentValues result = null;
   1008 
   1009             // Loop through the ncv's, looking for an existing row
   1010             for (NamedContentValues namedContentValues: list) {
   1011                 Uri uri = namedContentValues.uri;
   1012                 ContentValues cv = namedContentValues.values;
   1013                 if (Data.CONTENT_URI.equals(uri)) {
   1014                     String mimeType = cv.getAsString(Data.MIMETYPE);
   1015                     if (mimeType.equals(contentItemType)) {
   1016                         if (stringType != null) {
   1017                             if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
   1018                                 result = namedContentValues;
   1019                             }
   1020                         // Note Email.TYPE could be ANY type column; they are all defined in
   1021                         // the private CommonColumns class in ContactsContract
   1022                         // We'll accept either type < 0 (don't care), cv doesn't have a type,
   1023                         // or the types are equal
   1024                         } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
   1025                                 cv.getAsInteger(Email.TYPE) == type) {
   1026                             result = namedContentValues;
   1027                         }
   1028                     }
   1029                 }
   1030             }
   1031 
   1032             // If we've found an existing data row, we'll delete it.  Any rows left at the
   1033             // end should be deleted...
   1034             if (result != null) {
   1035                 list.remove(result);
   1036             }
   1037 
   1038             // Return the row found (or null)
   1039             return result;
   1040         }
   1041 
   1042         /**
   1043          * Given the list of NamedContentValues for an entity and a mime type
   1044          * gather all of the matching NCV's, returning them
   1045          * @param list the list of NCV's from the contact entity
   1046          * @param contentItemType the mime type we're looking for
   1047          * @param type the subtype (e.g. HOME, WORK, etc.)
   1048          * @return the matching NCVs
   1049          */
   1050         private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
   1051                 int type, String contentItemType) {
   1052             ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
   1053 
   1054             // Loop through the ncv's, looking for an existing row
   1055             for (NamedContentValues namedContentValues: list) {
   1056                 Uri uri = namedContentValues.uri;
   1057                 ContentValues cv = namedContentValues.values;
   1058                 if (Data.CONTENT_URI.equals(uri)) {
   1059                     String mimeType = cv.getAsString(Data.MIMETYPE);
   1060                     if (mimeType.equals(contentItemType)) {
   1061                         if (type != -1) {
   1062                             int subtype = cv.getAsInteger(Phone.TYPE);
   1063                             if (type != subtype) {
   1064                                 continue;
   1065                             }
   1066                         }
   1067                         result.add(namedContentValues);
   1068                     }
   1069                 }
   1070             }
   1071 
   1072             // If we've found an existing data row, we'll delete it.  Any rows left at the
   1073             // end should be deleted...
   1074             if (result != null) {
   1075                 list.remove(result);
   1076             }
   1077 
   1078             // Return the row found (or null)
   1079             return result;
   1080         }
   1081 
   1082         /**
   1083          * Create a wrapper for a builder (insert or update) that also includes the NCV for
   1084          * an existing row of this type.   If the SmartBuilder's cv field is not null, then
   1085          * it represents the current (old) values of this field.  The caller can then check
   1086          * whether the field is now different and needs to be updated; if it's not different,
   1087          * the caller will simply return and not generate a new CPO.  Otherwise, the builder
   1088          * should have its content values set, and the built CPO should be added to the
   1089          * ContactOperations list.
   1090          *
   1091          * @param entity the contact entity (or null if this is a new contact)
   1092          * @param mimeType the mime type of this row
   1093          * @param type the subtype of this row
   1094          * @param stringType for groups, the name of the group (type will be ignored), or null
   1095          * @return the created SmartBuilder
   1096          */
   1097         public RowBuilder createBuilder(Entity entity, String mimeType, int type,
   1098                 String stringType) {
   1099             RowBuilder builder = null;
   1100 
   1101             if (entity != null) {
   1102                 NamedContentValues ncv =
   1103                     findTypedData(entity.getSubValues(), mimeType, type, stringType);
   1104                 if (ncv != null) {
   1105                     builder = new RowBuilder(
   1106                             ContentProviderOperation
   1107                                 .newUpdate(addCallerIsSyncAdapterParameter(
   1108                                     dataUriFromNamedContentValues(ncv))),
   1109                             ncv);
   1110                 }
   1111             }
   1112 
   1113             if (builder == null) {
   1114                 builder = newRowBuilder(entity, mimeType);
   1115             }
   1116 
   1117             // Return the appropriate builder (insert or update)
   1118             // Caller will fill in the appropriate values; 4 MIMETYPE is already set
   1119             return builder;
   1120         }
   1121 
   1122         private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
   1123             return createBuilder(entity, mimeType, type, null);
   1124         }
   1125 
   1126         private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
   1127             return createBuilder(entity, mimeType, -1, null);
   1128         }
   1129 
   1130         private RowBuilder newRowBuilder(Entity entity, String mimeType) {
   1131             // This is a new row; first get the contactId
   1132             // If the Contact is new, use the saved back value; otherwise the value in the entity
   1133             int contactId = mContactBackValue;
   1134             if (entity != null) {
   1135                 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
   1136             }
   1137 
   1138             // Create an insert operation with the proper contactId reference
   1139             RowBuilder builder =
   1140                 new RowBuilder(ContentProviderOperation.newInsert(
   1141                         addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
   1142             if (entity == null) {
   1143                 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
   1144             } else {
   1145                 builder.withValue(Data.RAW_CONTACT_ID, contactId);
   1146             }
   1147 
   1148             // Set the mime type of the row
   1149             builder.withValue(Data.MIMETYPE, mimeType);
   1150             return builder;
   1151         }
   1152 
   1153         /**
   1154          * Compare a column in a ContentValues with an (old) value, and see if they are the
   1155          * same.  For this purpose, null and an empty string are considered the same.
   1156          * @param cv a ContentValues object, from a NamedContentValues
   1157          * @param column a column that might be in the ContentValues
   1158          * @param oldValue an old value (or null) to check against
   1159          * @return whether the column's value in the ContentValues matches oldValue
   1160          */
   1161         private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
   1162             if (cv.containsKey(column)) {
   1163                 if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
   1164                     return true;
   1165                 }
   1166             } else if (oldValue == null || oldValue.length() == 0) {
   1167                 return true;
   1168             }
   1169             return false;
   1170         }
   1171 
   1172         public void addChildren(Entity entity, ArrayList<String> children) {
   1173             RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
   1174             int i = 0;
   1175             for (String child: children) {
   1176                 builder.withValue(EasChildren.ROWS[i++], child);
   1177             }
   1178             add(builder.build());
   1179         }
   1180 
   1181         public void addGroup(Entity entity, String group) {
   1182             RowBuilder builder =
   1183                 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
   1184             builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
   1185             add(builder.build());
   1186         }
   1187 
   1188         public void addBirthday(Entity entity, String birthday) {
   1189             RowBuilder builder =
   1190                     typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
   1191             ContentValues cv = builder.cv;
   1192             if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
   1193                 return;
   1194             }
   1195             builder.withValue(Event.START_DATE, birthday);
   1196             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
   1197             add(builder.build());
   1198         }
   1199 
   1200         public void addName(Entity entity, String prefix, String givenName, String familyName,
   1201                 String middleName, String suffix, String displayName, String yomiFirstName,
   1202                 String yomiLastName, String fileAs) {
   1203             RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
   1204             ContentValues cv = builder.cv;
   1205             if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
   1206                     cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
   1207                     cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
   1208                     cvCompareString(cv, StructuredName.PREFIX, prefix) &&
   1209                     cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
   1210                     cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
   1211                     //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
   1212                     cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
   1213                 return;
   1214             }
   1215             builder.withValue(StructuredName.GIVEN_NAME, givenName);
   1216             builder.withValue(StructuredName.FAMILY_NAME, familyName);
   1217             builder.withValue(StructuredName.MIDDLE_NAME, middleName);
   1218             builder.withValue(StructuredName.SUFFIX, suffix);
   1219             builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
   1220             builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
   1221             builder.withValue(StructuredName.PREFIX, prefix);
   1222             //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
   1223             add(builder.build());
   1224         }
   1225 
   1226         public void addPersonal(Entity entity, EasPersonal personal) {
   1227             RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
   1228             ContentValues cv = builder.cv;
   1229             if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
   1230                     cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
   1231                 return;
   1232             }
   1233             if (!personal.hasData()) {
   1234                 return;
   1235             }
   1236             builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
   1237             builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
   1238             add(builder.build());
   1239         }
   1240 
   1241         public void addBusiness(Entity entity, EasBusiness business) {
   1242             RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
   1243             ContentValues cv = builder.cv;
   1244             if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
   1245                     cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
   1246                     cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
   1247                 return;
   1248             }
   1249             if (!business.hasData()) {
   1250                 return;
   1251             }
   1252             builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
   1253             builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
   1254             builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
   1255             add(builder.build());
   1256         }
   1257 
   1258         public void addPhoto(Entity entity, String photo) {
   1259             RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
   1260             // We're always going to add this; it's not worth trying to figure out whether the
   1261             // picture is the same as the one stored.
   1262             byte[] pic = Base64.decode(photo, Base64.DEFAULT);
   1263             builder.withValue(Photo.PHOTO, pic);
   1264             add(builder.build());
   1265         }
   1266 
   1267         public void addPhone(Entity entity, int type, String phone) {
   1268             RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
   1269             ContentValues cv = builder.cv;
   1270             if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
   1271                 return;
   1272             }
   1273             builder.withValue(Phone.TYPE, type);
   1274             builder.withValue(Phone.NUMBER, phone);
   1275             add(builder.build());
   1276         }
   1277 
   1278         public void addWebpage(Entity entity, String url) {
   1279             RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
   1280             ContentValues cv = builder.cv;
   1281             if (cv != null && cvCompareString(cv, Website.URL, url)) {
   1282                 return;
   1283             }
   1284             builder.withValue(Website.TYPE, Website.TYPE_WORK);
   1285             builder.withValue(Website.URL, url);
   1286             add(builder.build());
   1287         }
   1288 
   1289         public void addRelation(Entity entity, int type, String value) {
   1290             RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
   1291             ContentValues cv = builder.cv;
   1292             if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
   1293                 return;
   1294             }
   1295             builder.withValue(Relation.TYPE, type);
   1296             builder.withValue(Relation.DATA, value);
   1297             add(builder.build());
   1298         }
   1299 
   1300         public void addNickname(Entity entity, String name) {
   1301             RowBuilder builder =
   1302                 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
   1303             ContentValues cv = builder.cv;
   1304             if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
   1305                 return;
   1306             }
   1307             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
   1308             builder.withValue(Nickname.NAME, name);
   1309             add(builder.build());
   1310         }
   1311 
   1312         public void addPostal(Entity entity, int type, String street, String city, String state,
   1313                 String country, String code) {
   1314             RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
   1315                     type);
   1316             ContentValues cv = builder.cv;
   1317             if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
   1318                     cvCompareString(cv, StructuredPostal.STREET, street) &&
   1319                     cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
   1320                     cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
   1321                     cvCompareString(cv, StructuredPostal.REGION, state)) {
   1322                 return;
   1323             }
   1324             builder.withValue(StructuredPostal.TYPE, type);
   1325             builder.withValue(StructuredPostal.CITY, city);
   1326             builder.withValue(StructuredPostal.STREET, street);
   1327             builder.withValue(StructuredPostal.COUNTRY, country);
   1328             builder.withValue(StructuredPostal.POSTCODE, code);
   1329             builder.withValue(StructuredPostal.REGION, state);
   1330             add(builder.build());
   1331         }
   1332 
   1333        /**
   1334          * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
   1335          * match them with existing rows; if there's a match, everything's great.  Otherwise, we
   1336          * either need to add a new row for the data, or we have to replace an existing one
   1337          * that no longer matches.  This is similar to the way Emails are handled.
   1338          */
   1339         public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
   1340                 int type, int maxRows) {
   1341             // Make a list of all same type rows in the existing entity
   1342             ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
   1343             ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
   1344             if (entity != null) {
   1345                 oldValues = findUntypedData(entityValues, type, mimeType);
   1346                 entityValues = entity.getSubValues();
   1347             }
   1348 
   1349             // These will be rows needing replacement with new values
   1350             ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
   1351 
   1352             // The count of existing rows
   1353             int numRows = oldValues.size();
   1354             for (UntypedRow row: rows) {
   1355                 boolean found = false;
   1356                 // If we already have this row, mark it
   1357                 for (NamedContentValues ncv: oldValues) {
   1358                     ContentValues cv = ncv.values;
   1359                     String data = cv.getAsString(COMMON_DATA_ROW);
   1360                     int rowType = -1;
   1361                     if (cv.containsKey(COMMON_TYPE_ROW)) {
   1362                         rowType = cv.getAsInteger(COMMON_TYPE_ROW);
   1363                     }
   1364                     if (row.isSameAs(rowType, data)) {
   1365                         cv.put(FOUND_DATA_ROW, true);
   1366                         // Remove this to indicate it's still being used
   1367                         entityValues.remove(ncv);
   1368                         found = true;
   1369                         break;
   1370                     }
   1371                 }
   1372                 if (!found) {
   1373                     // If we don't, there are two possibilities
   1374                     if (numRows < maxRows) {
   1375                         // If there are available rows, add a new one
   1376                         RowBuilder builder = newRowBuilder(entity, mimeType);
   1377                         row.addValues(builder);
   1378                         add(builder.build());
   1379                         numRows++;
   1380                     } else {
   1381                         // Otherwise, say we need to replace a row with this
   1382                         rowsToReplace.add(row);
   1383                     }
   1384                 }
   1385             }
   1386 
   1387             // Go through rows needing replacement
   1388             for (UntypedRow row: rowsToReplace) {
   1389                 for (NamedContentValues ncv: oldValues) {
   1390                     ContentValues cv = ncv.values;
   1391                     // Find a row that hasn't been used (i.e. doesn't match current rows)
   1392                     if (!cv.containsKey(FOUND_DATA_ROW)) {
   1393                         // And update it
   1394                         RowBuilder builder = new RowBuilder(
   1395                                 ContentProviderOperation
   1396                                     .newUpdate(addCallerIsSyncAdapterParameter(
   1397                                         dataUriFromNamedContentValues(ncv))),
   1398                                 ncv);
   1399                         row.addValues(builder);
   1400                         add(builder.build());
   1401                     }
   1402                 }
   1403             }
   1404         }
   1405 
   1406         public void addOrganization(Entity entity, int type, String company, String title,
   1407                 String department, String yomiCompanyName, String officeLocation) {
   1408             RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
   1409             ContentValues cv = builder.cv;
   1410             if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
   1411                     cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
   1412                     cvCompareString(cv, Organization.DEPARTMENT, department) &&
   1413                     cvCompareString(cv, Organization.TITLE, title) &&
   1414                     cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
   1415                 return;
   1416             }
   1417             builder.withValue(Organization.TYPE, type);
   1418             builder.withValue(Organization.COMPANY, company);
   1419             builder.withValue(Organization.TITLE, title);
   1420             builder.withValue(Organization.DEPARTMENT, department);
   1421             builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
   1422             builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
   1423             add(builder.build());
   1424         }
   1425 
   1426         public void addNote(Entity entity, String note) {
   1427             RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
   1428             ContentValues cv = builder.cv;
   1429             if (note == null) return;
   1430             note = note.replaceAll("\r\n", "\n");
   1431             if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
   1432                 return;
   1433             }
   1434 
   1435             // Reject notes with nothing in them.  Often, we get something from Outlook when
   1436             // nothing was ever entered.  Sigh.
   1437             int len = note.length();
   1438             int i = 0;
   1439             for (; i < len; i++) {
   1440                 char c = note.charAt(i);
   1441                 if (!Character.isWhitespace(c)) {
   1442                     break;
   1443                 }
   1444             }
   1445             if (i == len) return;
   1446 
   1447             builder.withValue(Note.NOTE, note);
   1448             add(builder.build());
   1449         }
   1450     }
   1451 
   1452     /**
   1453      * Generate the uri for the data row associated with this NamedContentValues object
   1454      * @param ncv the NamedContentValues object
   1455      * @return a uri that can be used to refer to this row
   1456      */
   1457     public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
   1458         long id = ncv.values.getAsLong(RawContacts._ID);
   1459         Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
   1460         return dataUri;
   1461     }
   1462 
   1463     @Override
   1464     public void cleanup() {
   1465         // Mark the changed contacts dirty = 0
   1466         // Permanently delete the user deletions
   1467         ContactOperations ops = new ContactOperations();
   1468         for (Long id: mUpdatedIdList) {
   1469             ops.add(ContentProviderOperation
   1470                     .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
   1471                             .buildUpon()
   1472                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
   1473                             .build())
   1474                     .withValue(RawContacts.DIRTY, 0).build());
   1475         }
   1476         for (Long id: mDeletedIdList) {
   1477             ops.add(ContentProviderOperation
   1478                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
   1479                             .buildUpon()
   1480                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
   1481                             .build())
   1482                     .build());
   1483         }
   1484         ops.execute();
   1485         ContentResolver cr = mContext.getContentResolver();
   1486         if (mGroupsUsed) {
   1487             // Make sure the title column is set for all of our groups
   1488             // And that all of our groups are visible
   1489             // TODO Perhaps the visible part should only happen when the group is created, but
   1490             // this is fine for now.
   1491             Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
   1492             Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
   1493                     Groups.TITLE + " IS NULL", null, null);
   1494             ContentValues values = new ContentValues();
   1495             values.put(Groups.GROUP_VISIBLE, 1);
   1496             try {
   1497                 while (c.moveToNext()) {
   1498                     String sourceId = c.getString(0);
   1499                     values.put(Groups.TITLE, sourceId);
   1500                     cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
   1501                             Groups.SOURCE_ID + "=?", new String[] {sourceId});
   1502                 }
   1503             } finally {
   1504                 c.close();
   1505             }
   1506         }
   1507     }
   1508 
   1509     @Override
   1510     public String getCollectionName() {
   1511         return "Contacts";
   1512     }
   1513 
   1514     private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
   1515             throws IOException {
   1516         // Get both parts of the email address (a newly created one in the UI won't have a name)
   1517         String addr = cv.getAsString(Email.DATA);
   1518         String name = cv.getAsString(Email.DISPLAY_NAME);
   1519         if (name == null) {
   1520             if (displayName != null) {
   1521                 name = displayName;
   1522             } else {
   1523                 name = addr;
   1524             }
   1525         }
   1526         // Compose address from name and addr
   1527         if (addr != null) {
   1528             String value = '\"' + name + "\" <" + addr + '>';
   1529             if (count < MAX_EMAIL_ROWS) {
   1530                 s.data(EMAIL_TAGS[count], value);
   1531             }
   1532         }
   1533     }
   1534 
   1535     private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
   1536         String value = cv.getAsString(Im.DATA);
   1537         if (value == null) return;
   1538         if (count < MAX_IM_ROWS) {
   1539             s.data(IM_TAGS[count], value);
   1540         }
   1541     }
   1542 
   1543     private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
   1544             throws IOException{
   1545         if (cv.containsKey(StructuredPostal.CITY)) {
   1546             s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
   1547         }
   1548         if (cv.containsKey(StructuredPostal.COUNTRY)) {
   1549             s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
   1550         }
   1551         if (cv.containsKey(StructuredPostal.POSTCODE)) {
   1552             s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
   1553         }
   1554         if (cv.containsKey(StructuredPostal.REGION)) {
   1555             s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
   1556         }
   1557         if (cv.containsKey(StructuredPostal.STREET)) {
   1558             s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
   1559         }
   1560     }
   1561 
   1562     private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
   1563         switch (cv.getAsInteger(StructuredPostal.TYPE)) {
   1564             case StructuredPostal.TYPE_HOME:
   1565                 sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
   1566                 break;
   1567             case StructuredPostal.TYPE_WORK:
   1568                 sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
   1569                 break;
   1570             case StructuredPostal.TYPE_OTHER:
   1571                 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
   1572                 break;
   1573             default:
   1574                 break;
   1575         }
   1576     }
   1577 
   1578     private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
   1579         String displayName = null;
   1580         if (cv.containsKey(StructuredName.FAMILY_NAME)) {
   1581             s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME));
   1582         }
   1583         if (cv.containsKey(StructuredName.GIVEN_NAME)) {
   1584             s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME));
   1585         }
   1586         if (cv.containsKey(StructuredName.MIDDLE_NAME)) {
   1587             s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME));
   1588         }
   1589         if (cv.containsKey(StructuredName.SUFFIX)) {
   1590             s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX));
   1591         }
   1592         if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
   1593             s.data(Tags.CONTACTS_YOMI_FIRST_NAME,
   1594                     cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
   1595         }
   1596         if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) {
   1597             s.data(Tags.CONTACTS_YOMI_LAST_NAME,
   1598                     cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
   1599         }
   1600         if (cv.containsKey(StructuredName.PREFIX)) {
   1601             s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX));
   1602         }
   1603         if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
   1604             displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
   1605             s.data(Tags.CONTACTS_FILE_AS, displayName);
   1606         }
   1607         return displayName;
   1608     }
   1609 
   1610     private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
   1611         if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) {
   1612             s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME));
   1613         }
   1614         if (cv.containsKey(EasBusiness.CUSTOMER_ID)) {
   1615             s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID));
   1616         }
   1617         if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) {
   1618             s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID));
   1619         }
   1620     }
   1621 
   1622     private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
   1623         if (cv.containsKey(EasPersonal.ANNIVERSARY)) {
   1624             s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY));
   1625         }
   1626         if (cv.containsKey(EasPersonal.FILE_AS)) {
   1627             s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS));
   1628         }
   1629     }
   1630 
   1631     private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
   1632         if (cv.containsKey(Event.START_DATE)) {
   1633             s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Event.START_DATE));
   1634         }
   1635     }
   1636 
   1637     private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
   1638         if (cv.containsKey(Photo.PHOTO)) {
   1639             byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
   1640             String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
   1641             s.data(Tags.CONTACTS_PICTURE, pic);
   1642         } else {
   1643             // Send an empty tag, which signals the server to delete any pre-existing photo
   1644             s.tag(Tags.CONTACTS_PICTURE);
   1645         }
   1646     }
   1647 
   1648     private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
   1649         if (cv.containsKey(Organization.TITLE)) {
   1650             s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
   1651         }
   1652         if (cv.containsKey(Organization.COMPANY)) {
   1653             s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY));
   1654         }
   1655         if (cv.containsKey(Organization.DEPARTMENT)) {
   1656             s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT));
   1657         }
   1658         if (cv.containsKey(Organization.OFFICE_LOCATION)) {
   1659             s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION));
   1660         }
   1661     }
   1662 
   1663     private void sendNickname(Serializer s, ContentValues cv) throws IOException {
   1664         if (cv.containsKey(Nickname.NAME)) {
   1665             s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME));
   1666         }
   1667     }
   1668 
   1669     private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
   1670         if (cv.containsKey(Website.URL)) {
   1671             s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL));
   1672         }
   1673     }
   1674 
   1675     private void sendNote(Serializer s, ContentValues cv) throws IOException {
   1676         // Even when there is no local note, we must explicitly upsync an empty note,
   1677         // which is the only way to force the server to delete any pre-existing note.
   1678         String note = "";
   1679         if (cv.containsKey(Note.NOTE)) {
   1680             // EAS won't accept note data with raw newline characters
   1681             note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
   1682         }
   1683         // Format of upsync data depends on protocol version
   1684         if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
   1685             s.start(Tags.BASE_BODY);
   1686             s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
   1687             s.end();
   1688         } else {
   1689             s.data(Tags.CONTACTS_BODY, note);
   1690         }
   1691     }
   1692 
   1693     private void sendChildren(Serializer s, ContentValues cv) throws IOException {
   1694         boolean first = true;
   1695         for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
   1696             String row = EasChildren.ROWS[i];
   1697             if (cv.containsKey(row)) {
   1698                 if (first) {
   1699                     s.start(Tags.CONTACTS_CHILDREN);
   1700                     first = false;
   1701                 }
   1702                 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
   1703             }
   1704         }
   1705         if (!first) {
   1706             s.end();
   1707         }
   1708     }
   1709 
   1710     private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
   1711             throws IOException {
   1712         String value = cv.getAsString(Phone.NUMBER);
   1713         if (value == null) return;
   1714         switch (cv.getAsInteger(Phone.TYPE)) {
   1715             case Phone.TYPE_WORK:
   1716                 if (workCount < MAX_PHONE_ROWS) {
   1717                     s.data(WORK_PHONE_TAGS[workCount], value);
   1718                 }
   1719                 break;
   1720             case Phone.TYPE_MMS:
   1721                 s.data(Tags.CONTACTS2_MMS, value);
   1722                 break;
   1723             case Phone.TYPE_ASSISTANT:
   1724                 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
   1725                 break;
   1726             case Phone.TYPE_FAX_WORK:
   1727                 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
   1728                 break;
   1729             case Phone.TYPE_COMPANY_MAIN:
   1730                 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
   1731                 break;
   1732             case Phone.TYPE_HOME:
   1733                 if (homeCount < MAX_PHONE_ROWS) {
   1734                     s.data(HOME_PHONE_TAGS[homeCount], value);
   1735                 }
   1736                 break;
   1737             case Phone.TYPE_MOBILE:
   1738                 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
   1739                 break;
   1740             case Phone.TYPE_CAR:
   1741                 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
   1742                 break;
   1743             case Phone.TYPE_PAGER:
   1744                 s.data(Tags.CONTACTS_PAGER_NUMBER, value);
   1745                 break;
   1746             case Phone.TYPE_RADIO:
   1747                 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
   1748                 break;
   1749             case Phone.TYPE_FAX_HOME:
   1750                 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
   1751                 break;
   1752             default:
   1753                 break;
   1754         }
   1755     }
   1756 
   1757     private void sendRelation(Serializer s, ContentValues cv) throws IOException {
   1758         String value = cv.getAsString(Relation.DATA);
   1759         if (value == null) return;
   1760         switch (cv.getAsInteger(Relation.TYPE)) {
   1761             case Relation.TYPE_ASSISTANT:
   1762                 s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
   1763                 break;
   1764             case Relation.TYPE_MANAGER:
   1765                 s.data(Tags.CONTACTS2_MANAGER_NAME, value);
   1766                 break;
   1767             case Relation.TYPE_SPOUSE:
   1768                 s.data(Tags.CONTACTS_SPOUSE, value);
   1769                 break;
   1770             default:
   1771                 break;
   1772         }
   1773     }
   1774 
   1775     @Override
   1776     public boolean sendLocalChanges(Serializer s) throws IOException {
   1777         // First, let's find Contacts that have changed.
   1778         ContentResolver cr = mService.mContentResolver;
   1779         Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
   1780                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
   1781                 .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
   1782                         com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
   1783                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
   1784                 .build();
   1785 
   1786         if (getSyncKey().equals("0")) {
   1787             return false;
   1788         }
   1789 
   1790         // Get them all atomically
   1791         EntityIterator ei = RawContacts.newEntityIterator(
   1792                 cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
   1793         ContentValues cidValues = new ContentValues();
   1794         try {
   1795             boolean first = true;
   1796             final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
   1797             while (ei.hasNext()) {
   1798                 Entity entity = ei.next();
   1799                 // For each of these entities, create the change commands
   1800                 ContentValues entityValues = entity.getEntityValues();
   1801                 String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
   1802                 ArrayList<Integer> groupIds = new ArrayList<Integer>();
   1803                 if (first) {
   1804                     s.start(Tags.SYNC_COMMANDS);
   1805                     userLog("Sending Contacts changes to the server");
   1806                     first = false;
   1807                 }
   1808                 if (serverId == null) {
   1809                     // This is a new contact; create a clientId
   1810                     String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
   1811                     userLog("Creating new contact with clientId: ", clientId);
   1812                     s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
   1813                     // And save it in the raw contact
   1814                     cidValues.put(RawContacts.SYNC1, clientId);
   1815                     cr.update(ContentUris.
   1816                             withAppendedId(rawContactUri,
   1817                                     entityValues.getAsLong(RawContacts._ID)),
   1818                                     cidValues, null, null);
   1819                 } else {
   1820                     if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
   1821                         userLog("Deleting contact with serverId: ", serverId);
   1822                         s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
   1823                         mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
   1824                         continue;
   1825                     }
   1826                     userLog("Upsync change to contact with serverId: " + serverId);
   1827                     s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
   1828                 }
   1829                 s.start(Tags.SYNC_APPLICATION_DATA);
   1830                 // Write out the data here
   1831                 int imCount = 0;
   1832                 int emailCount = 0;
   1833                 int homePhoneCount = 0;
   1834                 int workPhoneCount = 0;
   1835                 String displayName = null;
   1836                 ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
   1837                 for (NamedContentValues ncv: entity.getSubValues()) {
   1838                     ContentValues cv = ncv.values;
   1839                     String mimeType = cv.getAsString(Data.MIMETYPE);
   1840                     if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
   1841                         emailValues.add(cv);
   1842                     } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
   1843                         sendNickname(s, cv);
   1844                     } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
   1845                         sendChildren(s, cv);
   1846                     } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
   1847                         sendBusiness(s, cv);
   1848                     } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
   1849                         sendWebpage(s, cv);
   1850                     } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
   1851                         sendPersonal(s, cv);
   1852                     } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
   1853                         sendPhone(s, cv, workPhoneCount, homePhoneCount);
   1854                         int type = cv.getAsInteger(Phone.TYPE);
   1855                         if (type == Phone.TYPE_HOME) homePhoneCount++;
   1856                         if (type == Phone.TYPE_WORK) workPhoneCount++;
   1857                     } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
   1858                         sendRelation(s, cv);
   1859                     } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
   1860                         displayName = sendStructuredName(s, cv);
   1861                     } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
   1862                         sendStructuredPostal(s, cv);
   1863                     } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
   1864                         sendOrganization(s, cv);
   1865                     } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
   1866                         sendIm(s, cv, imCount++);
   1867                     } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
   1868                         Integer eventType = cv.getAsInteger(Event.TYPE);
   1869                         if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
   1870                             sendBirthday(s, cv);
   1871                         }
   1872                     } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
   1873                         // We must gather these, and send them together (below)
   1874                         groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
   1875                     } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
   1876                         sendNote(s, cv);
   1877                     } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
   1878                         sendPhoto(s, cv);
   1879                     } else {
   1880                         userLog("Contacts upsync, unknown data: ", mimeType);
   1881                     }
   1882                 }
   1883 
   1884                 // We do the email rows last, because we need to make sure we've found the
   1885                 // displayName (if one exists); this would be in a StructuredName rnow
   1886                 for (ContentValues cv: emailValues) {
   1887                     sendEmail(s, cv, emailCount++, displayName);
   1888                 }
   1889 
   1890                 // Now, we'll send up groups, if any
   1891                 if (!groupIds.isEmpty()) {
   1892                     boolean groupFirst = true;
   1893                     for (int id: groupIds) {
   1894                         // Since we get id's from the provider, we need to find their names
   1895                         Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
   1896                                 GROUP_PROJECTION, null, null, null);
   1897                         try {
   1898                             // Presumably, this should always succeed, but ...
   1899                             if (c.moveToFirst()) {
   1900                                 if (groupFirst) {
   1901                                     s.start(Tags.CONTACTS_CATEGORIES);
   1902                                     groupFirst = false;
   1903                                 }
   1904                                 s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
   1905                             }
   1906                         } finally {
   1907                             c.close();
   1908                         }
   1909                     }
   1910                     if (!groupFirst) {
   1911                         s.end();
   1912                     }
   1913                 }
   1914                 s.end().end(); // ApplicationData & Change
   1915                 mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
   1916             }
   1917             if (!first) {
   1918                 s.end(); // Commands
   1919             }
   1920         } finally {
   1921             ei.close();
   1922         }
   1923 
   1924         return false;
   1925     }
   1926 }
   1927