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