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