ContactsSyncAdapter.java revision 1368f7c03e121f3240cfdd1698b86715a2655729
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.codec.binary.Base64;
21import com.android.email.provider.EmailContent.Mailbox;
22import com.android.exchange.Eas;
23import com.android.exchange.EasSyncService;
24
25import android.content.ContentProviderClient;
26import android.content.ContentProviderOperation;
27import android.content.ContentProviderResult;
28import android.content.ContentResolver;
29import android.content.ContentUris;
30import android.content.ContentValues;
31import android.content.Entity;
32import android.content.EntityIterator;
33import android.content.OperationApplicationException;
34import android.content.ContentProviderOperation.Builder;
35import android.content.Entity.NamedContentValues;
36import android.database.Cursor;
37import android.net.Uri;
38import android.os.RemoteException;
39import android.provider.ContactsContract;
40import android.provider.SyncStateContract;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.Groups;
43import android.provider.ContactsContract.RawContacts;
44import android.provider.ContactsContract.Settings;
45import android.provider.ContactsContract.SyncState;
46import android.provider.ContactsContract.CommonDataKinds.Birthday;
47import android.provider.ContactsContract.CommonDataKinds.Email;
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;
62
63import java.io.IOException;
64import java.io.InputStream;
65import java.util.ArrayList;
66
67/**
68 * Sync adapter for EAS Contacts
69 *
70 */
71public class ContactsSyncAdapter extends AbstractSyncAdapter {
72
73    private static final String TAG = "EasContactsSyncAdapter";
74    private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
75    private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
76    private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
77    private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
78
79    private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
80
81    private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
82        Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
83        Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
84        Tags.CONTACTS_HOME_ADDRESS_STATE,
85        Tags.CONTACTS_HOME_ADDRESS_STREET};
86
87    private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
88        Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
89        Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
90        Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
91        Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
92
93    private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
94        Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
95        Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
96        Tags.CONTACTS_OTHER_ADDRESS_STATE,
97        Tags.CONTACTS_OTHER_ADDRESS_STREET};
98
99    private static final int MAX_IM_ROWS = 3;
100    private static final int MAX_EMAIL_ROWS = 3;
101    private static final int MAX_PHONE_ROWS = 2;
102    private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
103    private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
104
105    private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
106        Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
107
108    private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
109        Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
110
111    private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
112        Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
113
114    private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
115        Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
116
117    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
118    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
119
120    private boolean mGroupsUsed = false;
121
122    private android.accounts.Account mAccountManagerAccount;
123
124    public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
125        super(mailbox, service);
126    }
127
128    static Uri addCallerIsSyncAdapterParameter(Uri uri) {
129        return uri.buildUpon()
130                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
131                .build();
132    }
133
134    @Override
135    public boolean parse(InputStream is) throws IOException {
136        EasContactsSyncParser p = new EasContactsSyncParser(is, this);
137        return p.parse();
138    }
139
140    interface UntypedRow {
141        public void addValues(RowBuilder builder);
142        public boolean isSameAs(int type, String value);
143    }
144
145    /**
146     * We get our SyncKey from ContactsProvider.  If there's not one, we set it to "0" (the reset
147     * state) and save that away.
148     */
149    @Override
150    public String getSyncKey() throws IOException {
151        ContentProviderClient client =
152            mService.mContentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
153        try {
154            byte[] data = SyncStateContract.Helpers.get(client,
155                    ContactsContract.SyncState.CONTENT_URI, getAccountManagerAccount());
156            if (data == null || data.length == 0) {
157                // Initialize the SyncKey
158                setSyncKey("0", false);
159                // Make sure ungrouped contacts for Exchange are defaultly visible
160                ContentValues cv = new ContentValues();
161                cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
162                cv.put(Groups.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
163                cv.put(Settings.UNGROUPED_VISIBLE, true);
164                client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
165                return "0";
166            } else {
167                return new String(data);
168            }
169        } catch (RemoteException e) {
170            throw new IOException("Can't get SyncKey from ContactsProvider");
171        }
172    }
173
174    /**
175     * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
176     * cases, the SyncKey is set within ContactOperations
177     */
178    @Override
179    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
180        if ("0".equals(syncKey) || !inCommands) {
181            ContentProviderClient client =
182                mService.mContentResolver
183                    .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
184            try {
185                SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
186                        getAccountManagerAccount(), syncKey.getBytes());
187                userLog("SyncKey set to ", syncKey, " in ContactsProvider");
188           } catch (RemoteException e) {
189                throw new IOException("Can't set SyncKey in ContactsProvider");
190            }
191        }
192        mMailbox.mSyncKey = syncKey;
193    }
194
195    public android.accounts.Account getAccountManagerAccount() {
196        if (mAccountManagerAccount == null) {
197            mAccountManagerAccount =
198                new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
199        }
200        return mAccountManagerAccount;
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                                try {
742                                    EntityIterator entityIterator =
743                                        mContentResolver.queryEntities(ContentUris
744                                            .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
745                                            null, null, null);
746                                    if (entityIterator.hasNext()) {
747                                        entity = entityIterator.next();
748                                    }
749                                    userLog("Changing contact ", serverId);
750                                } catch (RemoteException e) {
751                                }
752                            }
753                        } finally {
754                            c.close();
755                        }
756                        break;
757                    case Tags.SYNC_APPLICATION_DATA:
758                        addData(serverId, ops, entity);
759                        break;
760                    default:
761                        skipTag();
762                }
763            }
764        }
765
766        @Override
767        public void commandsParser() throws IOException {
768            while (nextTag(Tags.SYNC_COMMANDS) != END) {
769                if (tag == Tags.SYNC_ADD) {
770                    addParser(ops);
771                    incrementChangeCount();
772                } else if (tag == Tags.SYNC_DELETE) {
773                    deleteParser(ops);
774                    incrementChangeCount();
775                } else if (tag == Tags.SYNC_CHANGE) {
776                    changeParser(ops);
777                    incrementChangeCount();
778                } else
779                    skipTag();
780            }
781        }
782
783        @Override
784        public void commit() throws IOException {
785           // Save the syncKey here, using the Helper provider by Contacts provider
786            userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
787            ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
788                    getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
789
790            // Execute these all at once...
791            ops.execute();
792
793            if (ops.mResults != null) {
794                ContentValues cv = new ContentValues();
795                cv.put(RawContacts.DIRTY, 0);
796                for (int i = 0; i < ops.mContactIndexCount; i++) {
797                    int index = ops.mContactIndexArray[i];
798                    Uri u = ops.mResults[index].uri;
799                    if (u != null) {
800                        String idString = u.getLastPathSegment();
801                        mContentResolver.update(
802                                addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
803                                RawContacts._ID + "=" + idString, null);
804                    }
805                }
806            }
807        }
808
809        public void addResponsesParser() throws IOException {
810            String serverId = null;
811            String clientId = null;
812            ContentValues cv = new ContentValues();
813            while (nextTag(Tags.SYNC_ADD) != END) {
814                switch (tag) {
815                    case Tags.SYNC_SERVER_ID:
816                        serverId = getValue();
817                        break;
818                    case Tags.SYNC_CLIENT_ID:
819                        clientId = getValue();
820                        break;
821                    case Tags.SYNC_STATUS:
822                        getValue();
823                        break;
824                    default:
825                        skipTag();
826                }
827            }
828
829            // This is theoretically impossible, but...
830            if (clientId == null || serverId == null) return;
831
832            Cursor c = getClientIdCursor(clientId);
833            try {
834                if (c.moveToFirst()) {
835                    cv.put(RawContacts.SOURCE_ID, serverId);
836                    cv.put(RawContacts.DIRTY, 0);
837                    ops.add(ContentProviderOperation.newUpdate(
838                            ContentUris.withAppendedId(
839                                    addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
840                                    c.getLong(0)))
841                            .withValues(cv)
842                            .build());
843                    userLog("New contact " + clientId + " was given serverId: " + serverId);
844                }
845            } finally {
846                c.close();
847            }
848        }
849
850        public void changeResponsesParser() throws IOException {
851            String serverId = null;
852            String status = null;
853            while (nextTag(Tags.SYNC_CHANGE) != END) {
854                switch (tag) {
855                    case Tags.SYNC_SERVER_ID:
856                        serverId = getValue();
857                        break;
858                    case Tags.SYNC_STATUS:
859                        status = getValue();
860                        break;
861                    default:
862                        skipTag();
863                }
864            }
865            if (serverId != null && status != null) {
866                userLog("Changed contact " + serverId + " failed with status: " + status);
867            }
868        }
869
870
871        @Override
872        public void responsesParser() throws IOException {
873            // Handle server responses here (for Add and Change)
874            while (nextTag(Tags.SYNC_RESPONSES) != END) {
875                if (tag == Tags.SYNC_ADD) {
876                    addResponsesParser();
877                } else if (tag == Tags.SYNC_CHANGE) {
878                    changeResponsesParser();
879                } else
880                    skipTag();
881            }
882        }
883    }
884
885
886    private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
887        return uri.buildUpon()
888            .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
889            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
890            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
891            .build();
892    }
893
894    /**
895     * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
896     * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
897     * represent the current values of that row, that can be compared against current values to
898     * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
899     * the Builder.
900     */
901    private class RowBuilder {
902        Builder builder;
903        ContentValues cv;
904
905        public RowBuilder(Builder _builder) {
906            builder = _builder;
907        }
908
909        public RowBuilder(Builder _builder, NamedContentValues _ncv) {
910            builder = _builder;
911            cv = _ncv.values;
912        }
913
914        RowBuilder withValues(ContentValues values) {
915            builder.withValues(values);
916            return this;
917        }
918
919        RowBuilder withValueBackReference(String key, int previousResult) {
920            builder.withValueBackReference(key, previousResult);
921            return this;
922        }
923
924        ContentProviderOperation build() {
925            return builder.build();
926        }
927
928        RowBuilder withValue(String key, Object value) {
929            builder.withValue(key, value);
930            return this;
931        }
932    }
933
934    private class ContactOperations extends ArrayList<ContentProviderOperation> {
935        private static final long serialVersionUID = 1L;
936        private int mCount = 0;
937        private int mContactBackValue = mCount;
938        // Make an array big enough for the PIM window (max items we can get)
939        private int[] mContactIndexArray =
940            new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)];
941        private int mContactIndexCount = 0;
942        private ContentProviderResult[] mResults = null;
943
944        @Override
945        public boolean add(ContentProviderOperation op) {
946            super.add(op);
947            mCount++;
948            return true;
949        }
950
951        public void newContact(String serverId) {
952            Builder builder = ContentProviderOperation
953                .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
954            ContentValues values = new ContentValues();
955            values.put(RawContacts.SOURCE_ID, serverId);
956            builder.withValues(values);
957            mContactBackValue = mCount;
958            mContactIndexArray[mContactIndexCount++] = mCount;
959            add(builder.build());
960        }
961
962        public void delete(long id) {
963            add(ContentProviderOperation
964                    .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
965                            .buildUpon()
966                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
967                            .build())
968                    .build());
969        }
970
971        public void execute() {
972            synchronized (mService.getSynchronizer()) {
973                if (!mService.isStopped()) {
974                    try {
975                        if (!isEmpty()) {
976                            mService.userLog("Executing ", size(), " CPO's");
977                            mResults = mContext.getContentResolver().applyBatch(
978                                    ContactsContract.AUTHORITY, this);
979                        }
980                    } catch (RemoteException e) {
981                        // There is nothing sensible to be done here
982                        Log.e(TAG, "problem inserting contact during server update", e);
983                    } catch (OperationApplicationException e) {
984                        // There is nothing sensible to be done here
985                        Log.e(TAG, "problem inserting contact during server update", e);
986                    }
987                }
988            }
989        }
990
991        /**
992         * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
993         * tries to find a match, returning it
994         * @param list the list of NCV's from the contact entity
995         * @param contentItemType the mime type we're looking for
996         * @param type the subtype (e.g. HOME, WORK, etc.)
997         * @return the matching NCV or null if not found
998         */
999        private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
1000                String contentItemType, int type, String stringType) {
1001            NamedContentValues result = null;
1002
1003            // Loop through the ncv's, looking for an existing row
1004            for (NamedContentValues namedContentValues: list) {
1005                Uri uri = namedContentValues.uri;
1006                ContentValues cv = namedContentValues.values;
1007                if (Data.CONTENT_URI.equals(uri)) {
1008                    String mimeType = cv.getAsString(Data.MIMETYPE);
1009                    if (mimeType.equals(contentItemType)) {
1010                        if (stringType != null) {
1011                            if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
1012                                result = namedContentValues;
1013                            }
1014                        // Note Email.TYPE could be ANY type column; they are all defined in
1015                        // the private CommonColumns class in ContactsContract
1016                        } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
1017                            result = namedContentValues;
1018                        }
1019                    }
1020                }
1021            }
1022
1023            // If we've found an existing data row, we'll delete it.  Any rows left at the
1024            // end should be deleted...
1025            if (result != null) {
1026                list.remove(result);
1027            }
1028
1029            // Return the row found (or null)
1030            return result;
1031        }
1032
1033        /**
1034         * Given the list of NamedContentValues for an entity and a mime type
1035         * gather all of the matching NCV's, returning them
1036         * @param list the list of NCV's from the contact entity
1037         * @param contentItemType the mime type we're looking for
1038         * @param type the subtype (e.g. HOME, WORK, etc.)
1039         * @return the matching NCVs
1040         */
1041        private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
1042                int type, String contentItemType) {
1043            ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
1044
1045            // Loop through the ncv's, looking for an existing row
1046            for (NamedContentValues namedContentValues: list) {
1047                Uri uri = namedContentValues.uri;
1048                ContentValues cv = namedContentValues.values;
1049                if (Data.CONTENT_URI.equals(uri)) {
1050                    String mimeType = cv.getAsString(Data.MIMETYPE);
1051                    if (mimeType.equals(contentItemType)) {
1052                        if (type != -1) {
1053                            int subtype = cv.getAsInteger(Phone.TYPE);
1054                            if (type != subtype) {
1055                                continue;
1056                            }
1057                        }
1058                        result.add(namedContentValues);
1059                    }
1060                }
1061            }
1062
1063            // If we've found an existing data row, we'll delete it.  Any rows left at the
1064            // end should be deleted...
1065            if (result != null) {
1066                list.remove(result);
1067            }
1068
1069            // Return the row found (or null)
1070            return result;
1071        }
1072
1073        /**
1074         * Create a wrapper for a builder (insert or update) that also includes the NCV for
1075         * an existing row of this type.   If the SmartBuilder's cv field is not null, then
1076         * it represents the current (old) values of this field.  The caller can then check
1077         * whether the field is now different and needs to be updated; if it's not different,
1078         * the caller will simply return and not generate a new CPO.  Otherwise, the builder
1079         * should have its content values set, and the built CPO should be added to the
1080         * ContactOperations list.
1081         *
1082         * @param entity the contact entity (or null if this is a new contact)
1083         * @param mimeType the mime type of this row
1084         * @param type the subtype of this row
1085         * @param stringType for groups, the name of the group (type will be ignored), or null
1086         * @return the created SmartBuilder
1087         */
1088        public RowBuilder createBuilder(Entity entity, String mimeType, int type,
1089                String stringType) {
1090            RowBuilder builder = null;
1091
1092            if (entity != null) {
1093                NamedContentValues ncv =
1094                    findTypedData(entity.getSubValues(), mimeType, type, stringType);
1095                if (ncv != null) {
1096                    builder = new RowBuilder(
1097                            ContentProviderOperation
1098                                .newUpdate(addCallerIsSyncAdapterParameter(
1099                                    dataUriFromNamedContentValues(ncv))),
1100                            ncv);
1101                }
1102            }
1103
1104            if (builder == null) {
1105                builder = newRowBuilder(entity, mimeType);
1106            }
1107
1108            // Return the appropriate builder (insert or update)
1109            // Caller will fill in the appropriate values; 4 MIMETYPE is already set
1110            return builder;
1111        }
1112
1113        private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
1114            return createBuilder(entity, mimeType, type, null);
1115        }
1116
1117        private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
1118            return createBuilder(entity, mimeType, -1, null);
1119        }
1120
1121        private RowBuilder newRowBuilder(Entity entity, String mimeType) {
1122            // This is a new row; first get the contactId
1123            // If the Contact is new, use the saved back value; otherwise the value in the entity
1124            int contactId = mContactBackValue;
1125            if (entity != null) {
1126                contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
1127            }
1128
1129            // Create an insert operation with the proper contactId reference
1130            RowBuilder builder =
1131                new RowBuilder(ContentProviderOperation.newInsert(
1132                        addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
1133            if (entity == null) {
1134                builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
1135            } else {
1136                builder.withValue(Data.RAW_CONTACT_ID, contactId);
1137            }
1138
1139            // Set the mime type of the row
1140            builder.withValue(Data.MIMETYPE, mimeType);
1141            return builder;
1142        }
1143
1144        /**
1145         * Compare a column in a ContentValues with an (old) value, and see if they are the
1146         * same.  For this purpose, null and an empty string are considered the same.
1147         * @param cv a ContentValues object, from a NamedContentValues
1148         * @param column a column that might be in the ContentValues
1149         * @param oldValue an old value (or null) to check against
1150         * @return whether the column's value in the ContentValues matches oldValue
1151         */
1152        private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
1153            if (cv.containsKey(column)) {
1154                if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
1155                    return true;
1156                }
1157            } else if (oldValue == null || oldValue.length() == 0) {
1158                return true;
1159            }
1160            return false;
1161        }
1162
1163        public void addChildren(Entity entity, ArrayList<String> children) {
1164            RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
1165            int i = 0;
1166            for (String child: children) {
1167                builder.withValue(EasChildren.ROWS[i++], child);
1168            }
1169            add(builder.build());
1170        }
1171
1172        public void addGroup(Entity entity, String group) {
1173            RowBuilder builder =
1174                createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
1175            builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
1176            add(builder.build());
1177        }
1178
1179        public void addBirthday(Entity entity, String birthday) {
1180            RowBuilder builder =
1181                createBuilder(entity, Birthday.CONTENT_ITEM_TYPE, -1, birthday);
1182            builder.withValue(Birthday.BIRTHDAY, birthday);
1183            add(builder.build());
1184        }
1185
1186        public void addName(Entity entity, String prefix, String givenName, String familyName,
1187                String middleName, String suffix, String displayName, String yomiFirstName,
1188                String yomiLastName, String fileAs) {
1189            RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
1190            ContentValues cv = builder.cv;
1191            if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
1192                    cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
1193                    cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
1194                    cvCompareString(cv, StructuredName.PREFIX, prefix) &&
1195                    cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
1196                    cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
1197                    //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
1198                    cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
1199                return;
1200            }
1201            builder.withValue(StructuredName.GIVEN_NAME, givenName);
1202            builder.withValue(StructuredName.FAMILY_NAME, familyName);
1203            builder.withValue(StructuredName.MIDDLE_NAME, middleName);
1204            builder.withValue(StructuredName.SUFFIX, suffix);
1205            builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
1206            builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
1207            builder.withValue(StructuredName.PREFIX, prefix);
1208            //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
1209            add(builder.build());
1210        }
1211
1212        public void addPersonal(Entity entity, EasPersonal personal) {
1213            RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
1214            ContentValues cv = builder.cv;
1215            if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
1216                    cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
1217                return;
1218            }
1219            if (!personal.hasData()) {
1220                return;
1221            }
1222            builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
1223            builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
1224            add(builder.build());
1225        }
1226
1227        public void addBusiness(Entity entity, EasBusiness business) {
1228            RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
1229            ContentValues cv = builder.cv;
1230            if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
1231                    cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
1232                    cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
1233                return;
1234            }
1235            if (!business.hasData()) {
1236                return;
1237            }
1238            builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
1239            builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
1240            builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
1241            add(builder.build());
1242        }
1243
1244        public void addPhoto(Entity entity, String photo) {
1245            RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
1246            // We're always going to add this; it's not worth trying to figure out whether the
1247            // picture is the same as the one stored.
1248            byte[] pic = Base64.decodeBase64(photo.getBytes());
1249            builder.withValue(Photo.PHOTO, pic);
1250            add(builder.build());
1251        }
1252
1253        public void addPhone(Entity entity, int type, String phone) {
1254            RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
1255            ContentValues cv = builder.cv;
1256            if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
1257                return;
1258            }
1259            builder.withValue(Phone.TYPE, type);
1260            builder.withValue(Phone.NUMBER, phone);
1261            add(builder.build());
1262        }
1263
1264        public void addWebpage(Entity entity, String url) {
1265            RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
1266            ContentValues cv = builder.cv;
1267            if (cv != null && cvCompareString(cv, Website.URL, url)) {
1268                return;
1269            }
1270            builder.withValue(Website.TYPE, Website.TYPE_WORK);
1271            builder.withValue(Website.URL, url);
1272            add(builder.build());
1273        }
1274
1275        public void addRelation(Entity entity, int type, String value) {
1276            RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
1277            ContentValues cv = builder.cv;
1278            if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
1279                return;
1280            }
1281            builder.withValue(Relation.TYPE, type);
1282            builder.withValue(Relation.DATA, value);
1283            add(builder.build());
1284        }
1285
1286        public void addNickname(Entity entity, String name) {
1287            RowBuilder builder =
1288                typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
1289            ContentValues cv = builder.cv;
1290            if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
1291                return;
1292            }
1293            builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1294            builder.withValue(Nickname.NAME, name);
1295            add(builder.build());
1296        }
1297
1298        public void addPostal(Entity entity, int type, String street, String city, String state,
1299                String country, String code) {
1300            RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
1301                    type);
1302            ContentValues cv = builder.cv;
1303            if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
1304                    cvCompareString(cv, StructuredPostal.STREET, street) &&
1305                    cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
1306                    cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
1307                    cvCompareString(cv, StructuredPostal.REGION, state)) {
1308                return;
1309            }
1310            builder.withValue(StructuredPostal.TYPE, type);
1311            builder.withValue(StructuredPostal.CITY, city);
1312            builder.withValue(StructuredPostal.STREET, street);
1313            builder.withValue(StructuredPostal.COUNTRY, country);
1314            builder.withValue(StructuredPostal.POSTCODE, code);
1315            builder.withValue(StructuredPostal.REGION, state);
1316            add(builder.build());
1317        }
1318
1319       /**
1320         * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
1321         * match them with existing rows; if there's a match, everything's great.  Otherwise, we
1322         * either need to add a new row for the data, or we have to replace an existing one
1323         * that no longer matches.  This is similar to the way Emails are handled.
1324         */
1325        public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
1326                int type, int maxRows) {
1327            // Make a list of all same type rows in the existing entity
1328            ArrayList<NamedContentValues> oldAccounts = new ArrayList<NamedContentValues>();
1329            if (entity != null) {
1330                oldAccounts = findUntypedData(entity.getSubValues(), type, mimeType);
1331            }
1332
1333            // These will be rows needing replacement with new values
1334            ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
1335
1336            // The count of existing rows
1337            int numRows = oldAccounts.size();
1338            for (UntypedRow row: rows) {
1339                boolean found = false;
1340                // If we already have this IM address, mark it
1341                for (NamedContentValues ncv: oldAccounts) {
1342                    ContentValues cv = ncv.values;
1343                    String data = cv.getAsString(COMMON_DATA_ROW);
1344                    int rowType = -1;
1345                    if (cv.containsKey(COMMON_TYPE_ROW)) {
1346                        rowType = cv.getAsInteger(COMMON_TYPE_ROW);
1347                    }
1348                    if (row.isSameAs(rowType, data)) {
1349                        cv.put(FOUND_DATA_ROW, true);
1350                        found = true;
1351                        break;
1352                    }
1353                }
1354                if (!found) {
1355                    // If we don't, there are two possibilities
1356                    if (numRows < maxRows) {
1357                        // If there are available rows, add a new one
1358                        RowBuilder builder = newRowBuilder(entity, mimeType);
1359                        row.addValues(builder);
1360                        add(builder.build());
1361                        numRows++;
1362                    } else {
1363                        // Otherwise, say we need to replace a row with this
1364                        rowsToReplace.add(row);
1365                    }
1366                }
1367            }
1368
1369            // Go through rows needing replacement
1370            for (UntypedRow row: rowsToReplace) {
1371                for (NamedContentValues ncv: oldAccounts) {
1372                    ContentValues cv = ncv.values;
1373                    // Find a row that hasn't been used (i.e. doesn't match current rows)
1374                    if (!cv.containsKey(FOUND_DATA_ROW)) {
1375                        // And update it
1376                        RowBuilder builder = new RowBuilder(
1377                                ContentProviderOperation
1378                                    .newUpdate(addCallerIsSyncAdapterParameter(
1379                                        dataUriFromNamedContentValues(ncv))),
1380                                ncv);
1381                        row.addValues(builder);
1382                        add(builder.build());
1383                    }
1384                }
1385            }
1386        }
1387
1388        public void addOrganization(Entity entity, int type, String company, String title,
1389                String department, String yomiCompanyName, String officeLocation) {
1390            RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
1391            ContentValues cv = builder.cv;
1392            if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
1393                    cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
1394                    cvCompareString(cv, Organization.DEPARTMENT, department) &&
1395                    cvCompareString(cv, Organization.TITLE, title) &&
1396                    cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
1397                return;
1398            }
1399            builder.withValue(Organization.TYPE, type);
1400            builder.withValue(Organization.COMPANY, company);
1401            builder.withValue(Organization.TITLE, title);
1402            builder.withValue(Organization.DEPARTMENT, department);
1403            builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
1404            builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
1405            add(builder.build());
1406        }
1407
1408        public void addNote(Entity entity, String note) {
1409            RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
1410            ContentValues cv = builder.cv;
1411            if (note == null) return;
1412            note = note.replaceAll("\r\n", "\n");
1413            if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
1414                return;
1415            }
1416
1417            // Reject notes with nothing in them.  Often, we get something from Outlook when
1418            // nothing was ever entered.  Sigh.
1419            int len = note.length();
1420            int i = 0;
1421            for (; i < len; i++) {
1422                char c = note.charAt(i);
1423                if (!Character.isWhitespace(c)) {
1424                    break;
1425                }
1426            }
1427            if (i == len) return;
1428
1429            builder.withValue(Note.NOTE, note);
1430            add(builder.build());
1431        }
1432    }
1433
1434    /**
1435     * Generate the uri for the data row associated with this NamedContentValues object
1436     * @param ncv the NamedContentValues object
1437     * @return a uri that can be used to refer to this row
1438     */
1439    public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
1440        long id = ncv.values.getAsLong(RawContacts._ID);
1441        Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
1442        return dataUri;
1443    }
1444
1445    @Override
1446    public void cleanup() {
1447        // Mark the changed contacts dirty = 0
1448        // Permanently delete the user deletions
1449        ContactOperations ops = new ContactOperations();
1450        for (Long id: mUpdatedIdList) {
1451            ops.add(ContentProviderOperation
1452                    .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
1453                            .buildUpon()
1454                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1455                            .build())
1456                    .withValue(RawContacts.DIRTY, 0).build());
1457        }
1458        for (Long id: mDeletedIdList) {
1459            ops.add(ContentProviderOperation
1460                    .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
1461                            .buildUpon()
1462                            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1463                            .build())
1464                    .build());
1465        }
1466        ops.execute();
1467        ContentResolver cr = mContext.getContentResolver();
1468        if (mGroupsUsed) {
1469            // Make sure the title column is set for all of our groups
1470            // And that all of our groups are visible
1471            // TODO Perhaps the visible part should only happen when the group is created, but
1472            // this is fine for now.
1473            Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
1474            Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
1475                    Groups.TITLE + " IS NULL", null, null);
1476            ContentValues values = new ContentValues();
1477            values.put(Groups.GROUP_VISIBLE, 1);
1478            try {
1479                while (c.moveToNext()) {
1480                    String sourceId = c.getString(0);
1481                    values.put(Groups.TITLE, sourceId);
1482                    cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
1483                            Groups.SOURCE_ID + "=?", new String[] {sourceId});
1484                }
1485            } finally {
1486                c.close();
1487            }
1488        }
1489    }
1490
1491    @Override
1492    public String getCollectionName() {
1493        return "Contacts";
1494    }
1495
1496    private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
1497            throws IOException {
1498        // Get both parts of the email address (a newly created one in the UI won't have a name)
1499        String addr = cv.getAsString(Email.DATA);
1500        String name = cv.getAsString(Email.DISPLAY_NAME);
1501        if (name == null) {
1502            if (displayName != null) {
1503                name = displayName;
1504            } else {
1505                name = addr;
1506            }
1507        }
1508        // Compose address from name and addr
1509        if (addr != null) {
1510            String value = '\"' + name + "\" <" + addr + '>';
1511            if (count < MAX_EMAIL_ROWS) {
1512                s.data(EMAIL_TAGS[count], value);
1513            }
1514        }
1515    }
1516
1517    private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
1518        String value = cv.getAsString(Im.DATA);
1519        if (value == null) return;
1520        if (count < MAX_IM_ROWS) {
1521            s.data(IM_TAGS[count], value);
1522        }
1523    }
1524
1525    private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
1526            throws IOException{
1527        if (cv.containsKey(StructuredPostal.CITY)) {
1528            s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
1529        }
1530        if (cv.containsKey(StructuredPostal.COUNTRY)) {
1531            s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
1532        }
1533        if (cv.containsKey(StructuredPostal.POSTCODE)) {
1534            s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
1535        }
1536        if (cv.containsKey(StructuredPostal.REGION)) {
1537            s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
1538        }
1539        if (cv.containsKey(StructuredPostal.STREET)) {
1540            s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
1541        }
1542    }
1543
1544    private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
1545        switch (cv.getAsInteger(StructuredPostal.TYPE)) {
1546            case StructuredPostal.TYPE_HOME:
1547                sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
1548                break;
1549            case StructuredPostal.TYPE_WORK:
1550                sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
1551                break;
1552            case StructuredPostal.TYPE_OTHER:
1553                sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
1554                break;
1555            default:
1556                break;
1557        }
1558    }
1559
1560    private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
1561        String displayName = null;
1562        if (cv.containsKey(StructuredName.FAMILY_NAME)) {
1563            s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME));
1564        }
1565        if (cv.containsKey(StructuredName.GIVEN_NAME)) {
1566            s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME));
1567        }
1568        if (cv.containsKey(StructuredName.MIDDLE_NAME)) {
1569            s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME));
1570        }
1571        if (cv.containsKey(StructuredName.SUFFIX)) {
1572            s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX));
1573        }
1574        if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
1575            s.data(Tags.CONTACTS_YOMI_FIRST_NAME,
1576                    cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
1577        }
1578        if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) {
1579            s.data(Tags.CONTACTS_YOMI_LAST_NAME,
1580                    cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
1581        }
1582        if (cv.containsKey(StructuredName.PREFIX)) {
1583            s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX));
1584        }
1585        if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
1586            displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
1587            s.data(Tags.CONTACTS_FILE_AS, displayName);
1588        }
1589        return displayName;
1590    }
1591
1592    private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
1593        if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) {
1594            s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME));
1595        }
1596        if (cv.containsKey(EasBusiness.CUSTOMER_ID)) {
1597            s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID));
1598        }
1599        if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) {
1600            s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID));
1601        }
1602    }
1603
1604    private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
1605        if (cv.containsKey(EasPersonal.ANNIVERSARY)) {
1606            s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY));
1607        }
1608        if (cv.containsKey(EasPersonal.FILE_AS)) {
1609            s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS));
1610        }
1611    }
1612
1613    private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
1614        if (cv.containsKey(Birthday.BIRTHDAY)) {
1615            s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Birthday.BIRTHDAY));
1616        }
1617    }
1618
1619    private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
1620        if (cv.containsKey(Organization.TITLE)) {
1621            s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
1622        }
1623        if (cv.containsKey(Organization.COMPANY)) {
1624            s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY));
1625        }
1626        if (cv.containsKey(Organization.DEPARTMENT)) {
1627            s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT));
1628        }
1629        if (cv.containsKey(Organization.OFFICE_LOCATION)) {
1630            s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION));
1631        }
1632    }
1633
1634    private void sendNickname(Serializer s, ContentValues cv) throws IOException {
1635        if (cv.containsKey(Nickname.NAME)) {
1636            s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME));
1637        }
1638    }
1639
1640    private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
1641        if (cv.containsKey(Website.URL)) {
1642            s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL));
1643        }
1644    }
1645
1646    private void sendNote(Serializer s, ContentValues cv) throws IOException {
1647        if (cv.containsKey(Note.NOTE)) {
1648            // EAS won't accept note data with raw newline characters
1649            String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
1650            // Format of upsync data depends on protocol version
1651            if (mService.mProtocolVersionDouble >= 12.0) {
1652                s.start(Tags.BASE_BODY);
1653                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
1654                s.end();
1655            } else {
1656                s.data(Tags.CONTACTS_BODY, note);
1657            }
1658        }
1659    }
1660
1661    private void sendChildren(Serializer s, ContentValues cv) throws IOException {
1662        boolean first = true;
1663        for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
1664            String row = EasChildren.ROWS[i];
1665            if (cv.containsKey(row)) {
1666                if (first) {
1667                    s.start(Tags.CONTACTS_CHILDREN);
1668                    first = false;
1669                }
1670                s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
1671            }
1672        }
1673        if (!first) {
1674            s.end();
1675        }
1676    }
1677
1678    private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
1679            throws IOException {
1680        String value = cv.getAsString(Phone.NUMBER);
1681        if (value == null) return;
1682        switch (cv.getAsInteger(Phone.TYPE)) {
1683            case Phone.TYPE_WORK:
1684                if (workCount < MAX_PHONE_ROWS) {
1685                    s.data(WORK_PHONE_TAGS[workCount], value);
1686                }
1687                break;
1688            case Phone.TYPE_MMS:
1689                s.data(Tags.CONTACTS2_MMS, value);
1690                break;
1691            case Phone.TYPE_ASSISTANT:
1692                s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
1693                break;
1694            case Phone.TYPE_FAX_WORK:
1695                s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
1696                break;
1697            case Phone.TYPE_COMPANY_MAIN:
1698                s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
1699                break;
1700            case Phone.TYPE_HOME:
1701                if (homeCount < MAX_PHONE_ROWS) {
1702                    s.data(HOME_PHONE_TAGS[homeCount], value);
1703                }
1704                break;
1705            case Phone.TYPE_MOBILE:
1706                s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
1707                break;
1708            case Phone.TYPE_CAR:
1709                s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
1710                break;
1711            case Phone.TYPE_PAGER:
1712                s.data(Tags.CONTACTS_PAGER_NUMBER, value);
1713                break;
1714            case Phone.TYPE_RADIO:
1715                s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
1716                break;
1717            case Phone.TYPE_FAX_HOME:
1718                s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
1719                break;
1720            default:
1721                break;
1722        }
1723    }
1724
1725    private void sendRelation(Serializer s, ContentValues cv) throws IOException {
1726        String value = cv.getAsString(Relation.DATA);
1727        if (value == null) return;
1728        switch (cv.getAsInteger(Relation.TYPE)) {
1729            case Relation.TYPE_ASSISTANT:
1730                s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
1731                break;
1732            case Relation.TYPE_MANAGER:
1733                s.data(Tags.CONTACTS2_MANAGER_NAME, value);
1734                break;
1735            case Relation.TYPE_SPOUSE:
1736                s.data(Tags.CONTACTS_SPOUSE, value);
1737                break;
1738            default:
1739                break;
1740        }
1741    }
1742
1743    @Override
1744    public boolean sendLocalChanges(Serializer s) throws IOException {
1745        // First, let's find Contacts that have changed.
1746        ContentResolver cr = mService.mContentResolver;
1747        Uri uri = RawContacts.CONTENT_URI.buildUpon()
1748                .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
1749                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
1750                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1751                .build();
1752
1753        if (getSyncKey().equals("0")) {
1754            return false;
1755        }
1756
1757        try {
1758            // Get them all atomically
1759            EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
1760            ContentValues cidValues = new ContentValues();
1761            try {
1762                boolean first = true;
1763                final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
1764                while (ei.hasNext()) {
1765                    Entity entity = ei.next();
1766                    // For each of these entities, create the change commands
1767                    ContentValues entityValues = entity.getEntityValues();
1768                    String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
1769                    ArrayList<Integer> groupIds = new ArrayList<Integer>();
1770                    if (first) {
1771                        s.start(Tags.SYNC_COMMANDS);
1772                        userLog("Sending Contacts changes to the server");
1773                        first = false;
1774                    }
1775                    if (serverId == null) {
1776                        // This is a new contact; create a clientId
1777                        String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
1778                        userLog("Creating new contact with clientId: ", clientId);
1779                        s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
1780                        // And save it in the raw contact
1781                        cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
1782                        cr.update(ContentUris.
1783                                withAppendedId(rawContactUri,
1784                                        entityValues.getAsLong(ContactsContract.RawContacts._ID)),
1785                                        cidValues, null, null);
1786                    } else {
1787                        if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
1788                            userLog("Deleting contact with serverId: ", serverId);
1789                            s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
1790                            mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
1791                            continue;
1792                        }
1793                        userLog("Upsync change to contact with serverId: " + serverId);
1794                        s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
1795                    }
1796                    s.start(Tags.SYNC_APPLICATION_DATA);
1797                    // Write out the data here
1798                    int imCount = 0;
1799                    int emailCount = 0;
1800                    int homePhoneCount = 0;
1801                    int workPhoneCount = 0;
1802                    String displayName = null;
1803                    ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
1804                    for (NamedContentValues ncv: entity.getSubValues()) {
1805                        ContentValues cv = ncv.values;
1806                        String mimeType = cv.getAsString(Data.MIMETYPE);
1807                        if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
1808                            emailValues.add(cv);
1809                        } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
1810                            sendNickname(s, cv);
1811                        } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
1812                            sendChildren(s, cv);
1813                        } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
1814                            sendBusiness(s, cv);
1815                        } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
1816                            sendWebpage(s, cv);
1817                        } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
1818                            sendPersonal(s, cv);
1819                        } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
1820                            sendPhone(s, cv, workPhoneCount, homePhoneCount);
1821                            int type = cv.getAsInteger(Phone.TYPE);
1822                            if (type == Phone.TYPE_HOME) homePhoneCount++;
1823                            if (type == Phone.TYPE_WORK) workPhoneCount++;
1824                        } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
1825                            sendRelation(s, cv);
1826                        } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
1827                            displayName = sendStructuredName(s, cv);
1828                        } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
1829                            sendStructuredPostal(s, cv);
1830                        } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
1831                            sendOrganization(s, cv);
1832                        } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
1833                            sendIm(s, cv, imCount++);
1834                        } else if (mimeType.equals(Birthday.CONTENT_ITEM_TYPE)) {
1835                            sendBirthday(s, cv);
1836                        } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
1837                            // We must gather these, and send them together (below)
1838                            groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
1839                        } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
1840                            sendNote(s, cv);
1841                        } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
1842                            // For now, the user can change the photo, but the change won't be
1843                            // uploaded.
1844                        } else {
1845                            userLog("Contacts upsync, unknown data: ", mimeType);
1846                        }
1847                    }
1848
1849                    // We do the email rows last, because we need to make sure we've found the
1850                    // displayName (if one exists); this would be in a StructuredName rnow
1851                    for (ContentValues cv: emailValues) {
1852                        sendEmail(s, cv, emailCount++, displayName);
1853                    }
1854
1855                    // Now, we'll send up groups, if any
1856                    if (!groupIds.isEmpty()) {
1857                        boolean groupFirst = true;
1858                        for (int id: groupIds) {
1859                            // Since we get id's from the provider, we need to find their names
1860                            Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
1861                                    GROUP_PROJECTION, null, null, null);
1862                            try {
1863                                // Presumably, this should always succeed, but ...
1864                                if (c.moveToFirst()) {
1865                                    if (groupFirst) {
1866                                        s.start(Tags.CONTACTS_CATEGORIES);
1867                                        groupFirst = false;
1868                                    }
1869                                    s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
1870                                }
1871                            } finally {
1872                                c.close();
1873                            }
1874                        }
1875                        if (!groupFirst) {
1876                            s.end();
1877                        }
1878                    }
1879                    s.end().end(); // ApplicationData & Change
1880                    mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
1881                }
1882                if (!first) {
1883                    s.end(); // Commands
1884                }
1885            } finally {
1886                ei.close();
1887            }
1888        } catch (RemoteException e) {
1889            Log.e(TAG, "Could not read dirty contacts.");
1890        }
1891
1892        return false;
1893    }
1894}
1895