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