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