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