ContactsSyncAdapter.java revision 0a4d05f0d8753c67364f7167e62cea82aef9a81e
1/*
2 * Copyright (C) 2008-2009 Marc Blank
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.exchange.adapter;
19
20import com.android.email.provider.EmailContent.Mailbox;
21import com.android.email.provider.EmailContent.MailboxColumns;
22import com.android.exchange.Eas;
23import com.android.exchange.EasSyncService;
24import com.android.exchange.utility.Base64;
25
26import android.content.ContentProviderOperation;
27import android.content.ContentProviderResult;
28import android.content.ContentResolver;
29import android.content.ContentUris;
30import android.content.ContentValues;
31import android.content.Entity;
32import android.content.EntityIterator;
33import android.content.OperationApplicationException;
34import android.content.ContentProviderOperation.Builder;
35import android.content.Entity.NamedContentValues;
36import android.database.Cursor;
37import android.net.Uri;
38import android.os.RemoteException;
39import android.provider.ContactsContract;
40import android.provider.ContactsContract.Data;
41import android.provider.ContactsContract.Groups;
42import android.provider.ContactsContract.RawContacts;
43import android.provider.ContactsContract.CommonDataKinds.Email;
44import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
45import android.provider.ContactsContract.CommonDataKinds.Im;
46import android.provider.ContactsContract.CommonDataKinds.Nickname;
47import android.provider.ContactsContract.CommonDataKinds.Note;
48import android.provider.ContactsContract.CommonDataKinds.Organization;
49import android.provider.ContactsContract.CommonDataKinds.Phone;
50import android.provider.ContactsContract.CommonDataKinds.Photo;
51import android.provider.ContactsContract.CommonDataKinds.StructuredName;
52import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
53import android.text.util.Rfc822Token;
54import android.text.util.Rfc822Tokenizer;
55import android.util.Log;
56
57import java.io.IOException;
58import java.io.InputStream;
59import java.util.ArrayList;
60
61/**
62 * Sync adapter for EAS Contacts
63 *
64 */
65public class ContactsSyncAdapter extends AbstractSyncAdapter {
66
67    private static final String TAG = "EasContactsSyncAdapter";
68    private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
69    private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
70    private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
71
72    // Note: These constants are likely to change; they are internal to this class now, but
73    // may end up in the provider.
74    private static final int TYPE_EMAIL1 = 20;
75    private static final int TYPE_EMAIL2 = 21;
76    private static final int TYPE_EMAIL3 = 22;
77
78    // We'll split email into two columns, the one that Contacts uses (just for the email address
79    // portion, and another one (the one defined here) for the display name
80    //private static final String EMAIL_DISPLAY_NAME = Data.SYNC1;
81
82    private static final int TYPE_IM1 = 23;
83    private static final int TYPE_IM2 = 24;
84    private static final int TYPE_IM3 = 25;
85
86    private static final int TYPE_WORK2 = 26;
87    private static final int TYPE_HOME2 = 27;
88    private static final int TYPE_CAR = 28;
89    private static final int TYPE_COMPANY_MAIN = 29;
90    private static final int TYPE_MMS = 30;
91    private static final int TYPE_RADIO = 31;
92    private static final int TYPE_ASSISTANT = 32;
93
94    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
95    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
96
97    public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
98        super(mailbox, service);
99    }
100
101    @Override
102    public boolean parse(InputStream is, EasSyncService service) throws IOException {
103        EasContactsSyncParser p = new EasContactsSyncParser(is, service);
104        return p.parse();
105    }
106
107    // YomiFirstName, YomiLastName, and YomiCompanyName are the names of EAS fields
108    // Yomi is a shortened form of yomigana, which is a Japanese phonetic rendering.
109    public static final class Yomi {
110        private Yomi() {}
111
112        /** MIME type used when storing this in data table. */
113        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_yomi";
114        public static final String FIRST_NAME = "data2";
115        public static final String LAST_NAME = "data3";
116        public static final String COMPANY_NAME = "data4";
117    }
118
119    public static final class EasChildren {
120        private EasChildren() {}
121
122        /** MIME type used when storing this in data table. */
123        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
124        public static final int MAX_CHILDREN = 8;
125        public static final String[] ROWS =
126            new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
127    }
128
129    public static final class EasPersonal {
130        String anniversary;
131        String birthday;
132        String fileAs;
133        String title;
134        String spouse;
135        String webpage;
136
137            /** MIME type used when storing this in data table. */
138        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
139        public static final String ANNIVERSARY = "data2";
140        public static final String BIRTHDAY = "data3";
141        public static final String FILE_AS = "data4";
142        public static final String TITLE = "data5";
143        public static final String SPOUSE = "data6";
144        public static final String WEBPAGE = "data7";
145
146        boolean hasData() {
147            return anniversary != null || birthday != null || fileAs != null || title != null
148                || spouse != null || webpage != null;
149        }
150    }
151
152    public static final class EasBusiness {
153        String assistantName;
154        String department;
155        String officeLocation;
156        String managerName;
157        String customerId;
158        String governmentId;
159        String accountName;
160
161        /** MIME type used when storing this in data table. */
162        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
163        public static final String ASSISTANT_NAME = "data2";
164        public static final String DEPARTMENT = "data3";
165        public static final String OFFICE_LOCATION = "data4";
166        public static final String MANAGER_NAME = "data5";
167        public static final String CUSTOMER_ID = "data6";
168        public static final String GOVERNMENT_ID = "data7";
169        public static final String ACCOUNT_NAME = "data8";
170
171        boolean hasData() {
172            return assistantName != null || department != null || officeLocation != null
173                || managerName != null || customerId != null || governmentId != null
174                || accountName != null;
175        }
176    }
177
178    public static final class Address {
179        String city;
180        String country;
181        String code;
182        String street;
183        String state;
184
185        boolean hasData() {
186            return city != null || country != null || code != null || state != null
187                || street != null;
188        }
189    }
190
191    class EasContactsSyncParser extends AbstractSyncParser {
192
193        String[] mBindArgument = new String[1];
194        String mMailboxIdAsString;
195        Uri mAccountUri;
196
197        public EasContactsSyncParser(InputStream in, EasSyncService service) throws IOException {
198            super(in, service);
199            mAccountUri = uriWithAccount(RawContacts.CONTENT_URI);
200        }
201
202        @Override
203        public void wipe() {
204            // TODO Use the bulk delete when the CP supports it
205//            mContentResolver.delete(mAccountUri.buildUpon()
206//                    .appendQueryParameter(ContactsContract.RawContacts.DELETE_PERMANENTLY, "true")
207//                    .build(), null, null);
208            Cursor c = mContentResolver.query(mAccountUri, new String[] {"_id"}, null, null, null);
209            try {
210                while (c.moveToNext()) {
211                    long id = c.getLong(0);
212                    mContentResolver.delete(ContentUris
213                            .withAppendedId(mAccountUri, id)
214                            .buildUpon().appendQueryParameter(
215                                    ContactsContract.RawContacts.DELETE_PERMANENTLY, "true")
216                            .build(), null, null);
217                }
218            } finally {
219                c.close();
220            }
221        }
222
223        public void addData(String serverId, ContactOperations ops, Entity entity)
224                throws IOException {
225            String firstName = null;
226            String lastName = null;
227            String middleName = null;
228            String suffix = null;
229            String companyName = null;
230            String yomiFirstName = null;
231            String yomiLastName = null;
232            String yomiCompanyName = null;
233            String title = null;
234            Address home = new Address();
235            Address work = new Address();
236            Address other = new Address();
237            EasBusiness business = new EasBusiness();
238            EasPersonal personal = new EasPersonal();
239            ArrayList<String> children = new ArrayList<String>();
240
241            if (entity == null) {
242                ops.newContact(serverId);
243            }
244
245            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
246                switch (tag) {
247                    case Tags.CONTACTS_FIRST_NAME:
248                        firstName = getValue();
249                        break;
250                    case Tags.CONTACTS_LAST_NAME:
251                        lastName = getValue();
252                        break;
253                    case Tags.CONTACTS_MIDDLE_NAME:
254                        middleName = getValue();
255                        break;
256                    case Tags.CONTACTS_SUFFIX:
257                        suffix = getValue();
258                        break;
259                    case Tags.CONTACTS_COMPANY_NAME:
260                        companyName = getValue();
261                        break;
262                    case Tags.CONTACTS_JOB_TITLE:
263                        title = getValue();
264                        break;
265                    case Tags.CONTACTS_EMAIL1_ADDRESS:
266                        ops.addEmail(entity, TYPE_EMAIL1, getValue());
267                        break;
268                    case Tags.CONTACTS_EMAIL2_ADDRESS:
269                        ops.addEmail(entity, TYPE_EMAIL2, getValue());
270                        break;
271                    case Tags.CONTACTS_EMAIL3_ADDRESS:
272                        ops.addEmail(entity, TYPE_EMAIL3, getValue());
273                        break;
274                    case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
275                        ops.addPhone(entity, TYPE_WORK2, getValue());
276                        break;
277                    case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
278                        ops.addPhone(entity, Phone.TYPE_WORK, getValue());
279                        break;
280                    case Tags.CONTACTS2_MMS:
281                        ops.addPhone(entity, TYPE_MMS, getValue());
282                        break;
283                    case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
284                        ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
285                        break;
286                    case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
287                        ops.addPhone(entity, TYPE_COMPANY_MAIN, getValue());
288                        break;
289                    case Tags.CONTACTS_HOME_FAX_NUMBER:
290                        ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
291                        break;
292                    case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
293                        ops.addPhone(entity, Phone.TYPE_HOME, getValue());
294                        break;
295                    case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
296                        ops.addPhone(entity, TYPE_HOME2, getValue());
297                        break;
298                    case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
299                        ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
300                        break;
301                    case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
302                        ops.addPhone(entity, TYPE_CAR, getValue());
303                        break;
304                    case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
305                        ops.addPhone(entity, TYPE_RADIO, getValue());
306                        break;
307                    case Tags.CONTACTS_PAGER_NUMBER:
308                        ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
309                        break;
310                    case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
311                        ops.addPhone(entity, TYPE_ASSISTANT, getValue());
312                        break;
313                    case Tags.CONTACTS2_IM_ADDRESS:
314                        ops.addIm(entity, TYPE_IM1, getValue());
315                        break;
316                    case Tags.CONTACTS2_IM_ADDRESS_2:
317                        ops.addIm(entity, TYPE_IM2, getValue());
318                        break;
319                    case Tags.CONTACTS2_IM_ADDRESS_3:
320                        ops.addIm(entity, TYPE_IM3, getValue());
321                        break;
322                    case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
323                        work.city = getValue();
324                        break;
325                    case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
326                        work.country = getValue();
327                        break;
328                    case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
329                        work.code = getValue();
330                        break;
331                    case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
332                        work.state = getValue();
333                        break;
334                    case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
335                        work.street = getValue();
336                        break;
337                    case Tags.CONTACTS_HOME_ADDRESS_CITY:
338                        home.city = getValue();
339                        break;
340                    case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
341                        home.country = getValue();
342                        break;
343                    case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
344                        home.code = getValue();
345                        break;
346                    case Tags.CONTACTS_HOME_ADDRESS_STATE:
347                        home.state = getValue();
348                        break;
349                    case Tags.CONTACTS_HOME_ADDRESS_STREET:
350                        home.street = getValue();
351                        break;
352                    case Tags.CONTACTS_OTHER_ADDRESS_CITY:
353                        other.city = getValue();
354                        break;
355                    case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
356                        other.country = getValue();
357                        break;
358                    case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
359                        other.code = getValue();
360                        break;
361                    case Tags.CONTACTS_OTHER_ADDRESS_STATE:
362                        other.state = getValue();
363                        break;
364                    case Tags.CONTACTS_OTHER_ADDRESS_STREET:
365                        other.street = getValue();
366                        break;
367
368                    case Tags.CONTACTS_CHILDREN:
369                        childrenParser(children);
370                        break;
371
372                    case Tags.CONTACTS_YOMI_COMPANY_NAME:
373                        yomiCompanyName = getValue();
374                        break;
375                    case Tags.CONTACTS_YOMI_FIRST_NAME:
376                        yomiFirstName = getValue();
377                        break;
378                    case Tags.CONTACTS_YOMI_LAST_NAME:
379                        yomiLastName = getValue();
380                        break;
381
382                    case Tags.CONTACTS2_NICKNAME:
383                        ops.addNickname(entity, getValue());
384                        break;
385
386                    // EAS Business
387                    case Tags.CONTACTS_ASSISTANT_NAME:
388                        business.assistantName = getValue();
389                        break;
390                    case Tags.CONTACTS_DEPARTMENT:
391                        business.department = getValue();
392                        break;
393                    case Tags.CONTACTS_OFFICE_LOCATION:
394                        business.officeLocation = getValue();
395                        break;
396                    case Tags.CONTACTS2_MANAGER_NAME:
397                        business.managerName = getValue();
398                        break;
399                    case Tags.CONTACTS2_CUSTOMER_ID:
400                        business.customerId = getValue();
401                        break;
402                    case Tags.CONTACTS2_GOVERNMENT_ID:
403                        business.governmentId = getValue();
404                        break;
405                    case Tags.CONTACTS2_ACCOUNT_NAME:
406                        business.accountName = getValue();
407                        break;
408
409                    // EAS Personal
410                    case Tags.CONTACTS_ANNIVERSARY:
411                        personal.anniversary = getValue();
412                        break;
413                    case Tags.CONTACTS_BIRTHDAY:
414                        personal.birthday = getValue();
415                        break;
416                    case Tags.CONTACTS_FILE_AS:
417                        personal.fileAs = getValue();
418                        break;
419                    case Tags.CONTACTS_TITLE:
420                        personal.title = getValue();
421                        break;
422                    case Tags.CONTACTS_SPOUSE:
423                        personal.spouse = getValue();
424                        break;
425                    case Tags.CONTACTS_WEBPAGE:
426                        personal.webpage = getValue();
427                        break;
428
429                    case Tags.CONTACTS_PICTURE:
430                        ops.addPhoto(entity, getValue());
431                        break;
432
433                    case Tags.BASE_BODY:
434                        ops.addNote(entity, bodyParser());
435                        break;
436                    case Tags.CONTACTS_BODY:
437                        ops.addNote(entity, getValue());
438                        break;
439
440                    // TODO Handle Categories/Category
441                    // If we don't handle this properly, we'll lose the information if/when we
442                    // upload changes to the server!
443                    case Tags.CONTACTS_CATEGORIES:
444                        categoriesParser(ops, entity);
445                        break;
446
447
448                    case Tags.CONTACTS_COMPRESSED_RTF:
449                        // We don't use this, and it isn't necessary to upload, so we'll ignore it
450                        skipTag();
451                        break;
452
453                    default:
454                        skipTag();
455                }
456            }
457
458            // We must have first name, last name, or company name
459            String name;
460            if (firstName != null || lastName != null) {
461                if (firstName == null) {
462                    name = lastName;
463                } else if (lastName == null) {
464                    name = firstName;
465                } else {
466                    name = firstName + ' ' + lastName;
467                }
468            } else if (companyName != null) {
469                name = companyName;
470            } else {
471                return;
472            }
473
474            ops.addName(entity, firstName, lastName, middleName, suffix, name);
475            ops.addYomi(entity, yomiFirstName, yomiLastName, yomiCompanyName);
476            ops.addBusiness(entity, business);
477            ops.addPersonal(entity, personal);
478
479            if (!children.isEmpty()) {
480                ops.addChildren(entity, children);
481            }
482
483            if (work.hasData()) {
484                ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
485                        work.state, work.country, work.code);
486            }
487            if (home.hasData()) {
488                ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
489                        home.state, home.country, home.code);
490            }
491            if (other.hasData()) {
492                ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
493                        other.state, other.country, other.code);
494            }
495
496            if (companyName != null) {
497                ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title);
498            }
499
500            if (entity != null) {
501                // We've been removing rows from the list as they've been found in the xml
502                // Any that are left must have been deleted on the server
503                ArrayList<NamedContentValues> ncvList = entity.getSubValues();
504                for (NamedContentValues ncv: ncvList) {
505                    // These rows need to be deleted...
506                    Uri u = dataUriFromNamedContentValues(ncv);
507                    ops.add(ContentProviderOperation.newDelete(u).build());
508                }
509            }
510        }
511
512        private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
513            while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
514                switch (tag) {
515                    case Tags.CONTACTS_CATEGORY:
516                        ops.addGroup(entity, getValue());
517                    default:
518                        skipTag();
519                }
520            }
521        }
522
523        private void childrenParser(ArrayList<String> children) throws IOException {
524            while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
525                switch (tag) {
526                    case Tags.CONTACTS_CHILD:
527                        if (children.size() < EasChildren.MAX_CHILDREN) {
528                            children.add(getValue());
529                        }
530                        break;
531                    default:
532                        skipTag();
533                }
534            }
535        }
536
537        private String bodyParser() throws IOException {
538            String body = null;
539            while (nextTag(Tags.BASE_BODY) != END) {
540                switch (tag) {
541                    case Tags.BASE_DATA:
542                        body = getValue();
543                        break;
544                    default:
545                        skipTag();
546                }
547            }
548            return body;
549        }
550
551        public void addParser(ContactOperations ops) throws IOException {
552            String serverId = null;
553            while (nextTag(Tags.SYNC_ADD) != END) {
554                switch (tag) {
555                    case Tags.SYNC_SERVER_ID: // same as
556                        serverId = getValue();
557                        break;
558                    case Tags.SYNC_APPLICATION_DATA:
559                        addData(serverId, ops, null);
560                        break;
561                    default:
562                        skipTag();
563                }
564            }
565        }
566
567        private Cursor getServerIdCursor(String serverId) {
568            mBindArgument[0] = serverId;
569            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
570                    mBindArgument, null);
571        }
572
573        public void deleteParser(ContactOperations ops) throws IOException {
574            while (nextTag(Tags.SYNC_DELETE) != END) {
575                switch (tag) {
576                    case Tags.SYNC_SERVER_ID:
577                        String serverId = getValue();
578                        // Find the message in this mailbox with the given serverId
579                        Cursor c = getServerIdCursor(serverId);
580                        try {
581                            if (c.moveToFirst()) {
582                                userLog("Deleting ", serverId);
583                                ops.delete(c.getLong(0));
584                            }
585                        } finally {
586                            c.close();
587                        }
588                        break;
589                    default:
590                        skipTag();
591                }
592            }
593        }
594
595        class ServerChange {
596            long id;
597            boolean read;
598
599            ServerChange(long _id, boolean _read) {
600                id = _id;
601                read = _read;
602            }
603        }
604
605        /**
606         * Changes are handled row by row, and only changed/new rows are acted upon
607         * @param ops the array of pending ContactProviderOperations.
608         * @throws IOException
609         */
610        public void changeParser(ContactOperations ops) throws IOException {
611            String serverId = null;
612            Entity entity = null;
613            while (nextTag(Tags.SYNC_CHANGE) != END) {
614                switch (tag) {
615                    case Tags.SYNC_SERVER_ID:
616                        serverId = getValue();
617                        Cursor c = getServerIdCursor(serverId);
618                        try {
619                            if (c.moveToFirst()) {
620                                // TODO Handle deleted individual rows...
621                                try {
622                                    EntityIterator entityIterator =
623                                        mContentResolver.queryEntities(ContentUris
624                                            .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
625                                            null, null, null);
626                                    if (entityIterator.hasNext()) {
627                                        entity = entityIterator.next();
628                                    }
629                                    userLog("Changing contact ", serverId);
630                                } catch (RemoteException e) {
631                                }
632                            }
633                        } finally {
634                            c.close();
635                        }
636                        break;
637                    case Tags.SYNC_APPLICATION_DATA:
638                        addData(serverId, ops, entity);
639                        break;
640                    default:
641                        skipTag();
642                }
643            }
644        }
645
646        @Override
647        public void commandsParser() throws IOException {
648            ContactOperations ops = new ContactOperations();
649            while (nextTag(Tags.SYNC_COMMANDS) != END) {
650                if (tag == Tags.SYNC_ADD) {
651                    addParser(ops);
652                    incrementChangeCount();
653                } else if (tag == Tags.SYNC_DELETE) {
654                    deleteParser(ops);
655                    incrementChangeCount();
656                } else if (tag == Tags.SYNC_CHANGE) {
657                    changeParser(ops);
658                    incrementChangeCount();
659                } else
660                    skipTag();
661            }
662
663            // Execute these all at once...
664            ops.execute();
665
666            if (ops.mResults != null) {
667                ContentValues cv = new ContentValues();
668                cv.put(RawContacts.DIRTY, 0);
669                for (int i = 0; i < ops.mContactIndexCount; i++) {
670                    int index = ops.mContactIndexArray[i];
671                    Uri u = ops.mResults[index].uri;
672                    if (u != null) {
673                        String idString = u.getLastPathSegment();
674                        mContentResolver.update(RawContacts.CONTENT_URI, cv,
675                                RawContacts._ID + "=" + idString, null);
676                    }
677                }
678            }
679
680            // Update the sync key in the database
681            userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
682            ContentValues cv = new ContentValues();
683            cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
684            Mailbox.update(mContext, Mailbox.CONTENT_URI, mMailbox.mId, cv);
685        }
686    }
687
688
689    private Uri uriWithAccount(Uri uri) {
690        return uri.buildUpon()
691            .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
692            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
693            .build();
694    }
695
696    /**
697     * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
698     * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
699     * represent the current values of that row, that can be compared against current values to
700     * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
701     * the Builder.
702     */
703    private class SmartBuilder {
704        Builder builder;
705        ContentValues cv;
706
707        public SmartBuilder(Builder _builder) {
708            builder = _builder;
709        }
710
711        public SmartBuilder(Builder _builder, NamedContentValues _ncv) {
712            builder = _builder;
713            cv = _ncv.values;
714        }
715
716        SmartBuilder withValues(ContentValues values) {
717            builder.withValues(values);
718            return this;
719        }
720
721        SmartBuilder withValueBackReference(String key, int previousResult) {
722            builder.withValueBackReference(key, previousResult);
723            return this;
724        }
725
726        ContentProviderOperation build() {
727            return builder.build();
728        }
729
730        SmartBuilder withValue(String key, Object value) {
731            builder.withValue(key, value);
732            return this;
733        }
734    }
735
736    private class ContactOperations extends ArrayList<ContentProviderOperation> {
737        private static final long serialVersionUID = 1L;
738        private int mCount = 0;
739        private int mContactBackValue = mCount;
740        // Make an array big enough for the PIM window (max items we can get)
741        private int[] mContactIndexArray =
742            new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)];
743        private int mContactIndexCount = 0;
744        private ContentProviderResult[] mResults = null;
745
746        @Override
747        public boolean add(ContentProviderOperation op) {
748            super.add(op);
749            mCount++;
750            return true;
751        }
752
753        public void newContact(String serverId) {
754            Builder builder = ContentProviderOperation
755                .newInsert(uriWithAccount(RawContacts.CONTENT_URI));
756            ContentValues values = new ContentValues();
757            values.put(RawContacts.SOURCE_ID, serverId);
758            builder.withValues(values);
759            mContactBackValue = mCount;
760            mContactIndexArray[mContactIndexCount++] = mCount;
761            add(builder.build());
762        }
763
764        public void delete(long id) {
765            add(ContentProviderOperation
766                    .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
767                            .buildUpon()
768                            .appendQueryParameter(ContactsContract.RawContacts.DELETE_PERMANENTLY,
769                                    "true")
770                            .build())
771                    .build());
772        }
773
774        public void execute() {
775            synchronized (mService.getSynchronizer()) {
776                if (!mService.isStopped()) {
777                    try {
778                        mService.userLog("Executing ", size(), " CPO's");
779                        mResults = mContext.getContentResolver().applyBatch(
780                                ContactsContract.AUTHORITY, this);
781                    } catch (RemoteException e) {
782                        // There is nothing sensible to be done here
783                        Log.e(TAG, "problem inserting contact during server update", e);
784                    } catch (OperationApplicationException e) {
785                        // There is nothing sensible to be done here
786                        Log.e(TAG, "problem inserting contact during server update", e);
787                    }
788                }
789            }
790        }
791
792        /**
793         * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
794         * tries to find a match, returning it
795         * @param list the list of NCV's from the contact entity
796         * @param contentItemType the mime type we're looking for
797         * @param type the subtype (e.g. HOME, WORK, etc.)
798         * @return the matching NCV or null if not found
799         */
800        private NamedContentValues findExistingData(ArrayList<NamedContentValues> list,
801                String contentItemType, int type, String stringType) {
802            NamedContentValues result = null;
803
804            // Loop through the ncv's, looking for an existing row
805            for (NamedContentValues namedContentValues: list) {
806                Uri uri = namedContentValues.uri;
807                ContentValues cv = namedContentValues.values;
808                if (Data.CONTENT_URI.equals(uri)) {
809                    String mimeType = cv.getAsString(Data.MIMETYPE);
810                    if (mimeType.equals(contentItemType)) {
811                        if (stringType != null) {
812                            if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
813                                result = namedContentValues;
814                            }
815                        // Note Email.TYPE could be ANY type column; they are all defined in
816                        // the private CommonColumns class in ContactsContract
817                        } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
818                            result = namedContentValues;
819                        }
820                    }
821                }
822            }
823
824            // TODO Handle deleted items
825            // If we've found an existing data row, we'll delete it.  Any rows left at the
826            // end should be deleted...
827            if (result != null) {
828                list.remove(result);
829            }
830
831            // Return the row found (or null)
832            return result;
833        }
834
835        /**
836         * Create a wrapper for a builder (insert or update) that also includes the NCV for
837         * an existing row of this type.   If the SmartBuilder's cv field is not null, then
838         * it represents the current (old) values of this field.  The caller can then check
839         * whether the field is now different and needs to be updated; if it's not different,
840         * the caller will simply return and not generate a new CPO.  Otherwise, the builder
841         * should have its content values set, and the built CPO should be added to the
842         * ContactOperations list.
843         *
844         * @param entity the contact entity (or null if this is a new contact)
845         * @param mimeType the mime type of this row
846         * @param type the subtype of this row
847         * @param stringType for groups, the name of the group (type will be ignored), or null
848         * @return the created SmartBuilder
849         */
850        public SmartBuilder createBuilder(Entity entity, String mimeType, int type) {
851            return createBuilder(entity, mimeType, type, null);
852        }
853
854        public SmartBuilder createBuilder(Entity entity, String mimeType, int type,
855                String stringType) {
856            int contactId = mContactBackValue;
857            SmartBuilder builder = null;
858
859            if (entity != null) {
860                NamedContentValues ncv =
861                    findExistingData(entity.getSubValues(), mimeType, type, stringType);
862                if (ncv != null) {
863                    builder = new SmartBuilder(
864                            ContentProviderOperation
865                                .newUpdate(dataUriFromNamedContentValues(ncv)),
866                            ncv);
867                } else {
868                    contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
869                }
870            }
871
872            if (builder == null) {
873                builder =
874                    new SmartBuilder(ContentProviderOperation.newInsert(Data.CONTENT_URI));
875                if (entity == null) {
876                    builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
877                } else {
878                    builder.withValue(Data.RAW_CONTACT_ID, contactId);
879                }
880
881                builder.withValue(Data.MIMETYPE, mimeType);
882            }
883
884            // Return the appropriate builder (insert or update)
885            // Caller will fill in the appropriate values; 4 MIMETYPE is already set
886            return builder;
887        }
888
889        /**
890         * Compare a column in a ContentValues with an (old) value, and see if they are the
891         * same.  For this purpose, null and an empty string are considered the same.
892         * @param cv a ContentValues object, from a NamedContentValues
893         * @param column a column that might be in the ContentValues
894         * @param oldValue an old value (or null) to check against
895         * @return whether the column's value in the ContentValues matches oldValue
896         */
897        private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
898            if (cv.containsKey(column)) {
899                if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
900                    return true;
901                }
902            } else if (oldValue == null || oldValue.length() == 0) {
903                return true;
904            }
905            return false;
906        }
907
908        public void addEmail(Entity entity, int type, String email) {
909            SmartBuilder builder = createBuilder(entity, Email.CONTENT_ITEM_TYPE, type);
910            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
911            // Can't happen, but belt & suspenders
912            if (tokens.length == 0) {
913                return;
914            }
915            Rfc822Token token = tokens[0];
916            String addr = token.getAddress();
917            String name = token.getName();
918            ContentValues cv = builder.cv;
919            if (cv != null && cvCompareString(cv, Email.DATA, addr)
920                    && cvCompareString(cv, Email.DISPLAY_NAME, name)) {
921                return;
922            }
923            builder.withValue(Email.TYPE, type);
924            builder.withValue(Email.DATA, addr);
925            builder.withValue(Email.DISPLAY_NAME, name);
926            add(builder.build());
927        }
928
929        public void addChildren(Entity entity, ArrayList<String> children) {
930            SmartBuilder builder = createBuilder(entity, EasChildren.CONTENT_ITEM_TYPE, -1);
931            int i = 0;
932            for (String child: children) {
933                builder.withValue(EasChildren.ROWS[i++], child);
934            }
935            add(builder.build());
936        }
937
938        public void addGroup(Entity entity, String group) {
939            SmartBuilder builder =
940                createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
941            builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
942            add(builder.build());
943        }
944
945        public void addName(Entity entity, String givenName, String familyName, String middleName,
946                String suffix, String displayName) {
947            SmartBuilder builder = createBuilder(entity, StructuredName.CONTENT_ITEM_TYPE, -1);
948            ContentValues cv = builder.cv;
949            if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
950                    cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
951                    cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
952                    cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
953                return;
954            }
955            builder.withValue(StructuredName.GIVEN_NAME, givenName);
956            builder.withValue(StructuredName.FAMILY_NAME, familyName);
957            builder.withValue(StructuredName.MIDDLE_NAME, middleName);
958            builder.withValue(StructuredName.SUFFIX, suffix);
959            add(builder.build());
960        }
961
962        public void addYomi(Entity entity, String firstName, String lastName, String companyName) {
963            SmartBuilder builder = createBuilder(entity, Yomi.CONTENT_ITEM_TYPE, -1);
964            ContentValues cv = builder.cv;
965            if (cv != null && cvCompareString(cv, Yomi.FIRST_NAME, firstName) &&
966                    cvCompareString(cv, Yomi.LAST_NAME, lastName) &&
967                    cvCompareString(cv, Yomi.COMPANY_NAME, companyName)) {
968                return;
969            }
970            builder.withValue(Yomi.FIRST_NAME, firstName);
971            builder.withValue(Yomi.LAST_NAME, lastName);
972            builder.withValue(Yomi.COMPANY_NAME, companyName);
973            add(builder.build());
974        }
975
976        public void addPersonal(Entity entity, EasPersonal personal) {
977            SmartBuilder builder = createBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE, -1);
978            ContentValues cv = builder.cv;
979            if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
980                    cvCompareString(cv, EasPersonal.BIRTHDAY, personal.birthday) &&
981                    cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs) &&
982                    cvCompareString(cv, EasPersonal.SPOUSE, personal.spouse) &&
983                    cvCompareString(cv, EasPersonal.TITLE, personal.title) &&
984                    cvCompareString(cv, EasPersonal.WEBPAGE, personal.webpage)) {
985                return;
986            }
987            if (!personal.hasData()) {
988                return;
989            }
990            builder.withValue(EasPersonal.BIRTHDAY, personal.birthday);
991            builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
992            builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
993            builder.withValue(EasPersonal.SPOUSE, personal.spouse);
994            builder.withValue(EasPersonal.TITLE, personal.title);
995            builder.withValue(EasPersonal.WEBPAGE, personal.webpage);
996            add(builder.build());
997        }
998
999        public void addBusiness(Entity entity, EasBusiness business) {
1000            SmartBuilder builder = createBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE, -1);
1001            ContentValues cv = builder.cv;
1002            if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
1003                    cvCompareString(cv, EasBusiness.ASSISTANT_NAME, business.assistantName) &&
1004                    cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
1005                    cvCompareString(cv, EasBusiness.DEPARTMENT, business.department) &&
1006                    cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId) &&
1007                    cvCompareString(cv, EasBusiness.MANAGER_NAME, business.managerName) &&
1008                    cvCompareString(cv, EasBusiness.OFFICE_LOCATION, business.officeLocation)) {
1009                return;
1010            }
1011            if (!business.hasData()) {
1012                return;
1013            }
1014            builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
1015            builder.withValue(EasBusiness.ASSISTANT_NAME, business.assistantName);
1016            builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
1017            builder.withValue(EasBusiness.DEPARTMENT, business.department);
1018            builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
1019            builder.withValue(EasBusiness.MANAGER_NAME, business.managerName);
1020            builder.withValue(EasBusiness.OFFICE_LOCATION, business.officeLocation);
1021            add(builder.build());
1022        }
1023
1024        public void addPhoto(Entity entity, String photo) {
1025            SmartBuilder builder = createBuilder(entity, Photo.CONTENT_ITEM_TYPE, -1);
1026            // We're always going to add this; it's not worth trying to figure out whether the
1027            // picture is the same as the one stored.
1028            byte[] pic = Base64.decode(photo);
1029            builder.withValue(Photo.PHOTO, pic);
1030            add(builder.build());
1031        }
1032
1033        public void addPhone(Entity entity, int type, String phone) {
1034            SmartBuilder builder = createBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
1035            ContentValues cv = builder.cv;
1036            if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
1037                return;
1038            }
1039            builder.withValue(Phone.TYPE, type);
1040            builder.withValue(Phone.NUMBER, phone);
1041            add(builder.build());
1042        }
1043
1044        public void addNickname(Entity entity, String name) {
1045            SmartBuilder builder =
1046                createBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
1047            ContentValues cv = builder.cv;
1048            if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
1049                return;
1050            }
1051            builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1052            builder.withValue(Nickname.NAME, name);
1053            add(builder.build());
1054        }
1055
1056        public void addPostal(Entity entity, int type, String street, String city, String state,
1057                String country, String code) {
1058            SmartBuilder builder = createBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
1059                    type);
1060            ContentValues cv = builder.cv;
1061            if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
1062                    cvCompareString(cv, StructuredPostal.STREET, street) &&
1063                    cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
1064                    cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
1065                    cvCompareString(cv, StructuredPostal.REGION, state)) {
1066                return;
1067            }
1068            builder.withValue(StructuredPostal.TYPE, type);
1069            builder.withValue(StructuredPostal.CITY, city);
1070            builder.withValue(StructuredPostal.STREET, street);
1071            builder.withValue(StructuredPostal.COUNTRY, country);
1072            builder.withValue(StructuredPostal.POSTCODE, code);
1073            builder.withValue(StructuredPostal.REGION, state);
1074            add(builder.build());
1075        }
1076
1077        public void addIm(Entity entity, int type, String account) {
1078            SmartBuilder builder = createBuilder(entity, Im.CONTENT_ITEM_TYPE, type);
1079            ContentValues cv = builder.cv;
1080            if (cv != null && cvCompareString(cv, Im.DATA, account)) {
1081                return;
1082            }
1083            builder.withValue(Im.TYPE, type);
1084            builder.withValue(Im.DATA, account);
1085            add(builder.build());
1086        }
1087
1088        public void addOrganization(Entity entity, int type, String company, String title) {
1089            SmartBuilder builder = createBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
1090            ContentValues cv = builder.cv;
1091            if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
1092                    cvCompareString(cv, Organization.TITLE, title)) {
1093                return;
1094            }
1095            builder.withValue(Organization.TYPE, type);
1096            builder.withValue(Organization.COMPANY, company);
1097            builder.withValue(Organization.TITLE, title);
1098            add(builder.build());
1099        }
1100
1101        public void addNote(Entity entity, String note) {
1102            SmartBuilder builder = createBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
1103            ContentValues cv = builder.cv;
1104            if (note != null) {
1105                note = note.replaceAll("\r\n", "\n");
1106            }
1107            if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
1108                return;
1109            }
1110            builder.withValue(Note.NOTE, note);
1111            add(builder.build());
1112        }
1113    }
1114
1115    /**
1116     * Generate the uri for the data row associated with this NamedContentValues object
1117     * @param ncv the NamedContentValues object
1118     * @return a uri that can be used to refer to this row
1119     */
1120    public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
1121        long id = ncv.values.getAsLong(RawContacts._ID);
1122        Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
1123        return dataUri;
1124    }
1125
1126    @Override
1127    public void cleanup(EasSyncService service) {
1128        // Mark the changed contacts dirty = 0
1129        // TODO Put this in a single batch
1130        ContactOperations ops = new ContactOperations();
1131        for (Long id: mUpdatedIdList) {
1132            ops.add(ContentProviderOperation
1133                    .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id))
1134                    .withValue(RawContacts.DIRTY, 0).build());
1135        }
1136
1137        ops.execute();
1138    }
1139
1140    @Override
1141    public String getCollectionName() {
1142        return "Contacts";
1143    }
1144
1145    private void sendEmail(Serializer s, ContentValues cv) throws IOException {
1146        // Get both parts of the email address (a newly created one in the UI won't have a name)
1147        String addr = cv.getAsString(Email.DATA);
1148        String name = cv.getAsString(Email.DISPLAY_NAME);
1149        // Don't crash if we don't have a name
1150        if (name == null) {
1151            name = "";
1152        }
1153        String value = null;
1154        // If there's no addr, just send an empty address (will delete it on the server)
1155        // Otherwise compose it from name and addr
1156        if (addr != null) {
1157            value = '\"' + name + "\" <" + addr + '>';
1158        }
1159        switch (cv.getAsInteger(Email.TYPE)) {
1160            case TYPE_EMAIL1:
1161                s.data(Tags.CONTACTS_EMAIL1_ADDRESS, value);
1162                break;
1163            case TYPE_EMAIL2:
1164                s.data(Tags.CONTACTS_EMAIL2_ADDRESS, value);
1165                break;
1166            case TYPE_EMAIL3:
1167                s.data(Tags.CONTACTS_EMAIL3_ADDRESS, value);
1168                break;
1169            default:
1170                break;
1171        }
1172    }
1173
1174    private void sendIm(Serializer s, ContentValues cv) throws IOException {
1175        String value = cv.getAsString(Email.DATA);
1176        switch (cv.getAsInteger(Email.TYPE)) {
1177            case TYPE_IM1:
1178                s.data(Tags.CONTACTS2_IM_ADDRESS, value);
1179                break;
1180            case TYPE_IM2:
1181                s.data(Tags.CONTACTS2_IM_ADDRESS_2, value);
1182                break;
1183            case TYPE_IM3:
1184                s.data(Tags.CONTACTS2_IM_ADDRESS_3, value);
1185                break;
1186            default:
1187                break;
1188        }
1189    }
1190
1191    private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
1192            throws IOException{
1193        if (cv.containsKey(StructuredPostal.CITY)) {
1194            s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
1195        }
1196        if (cv.containsKey(StructuredPostal.COUNTRY)) {
1197            s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
1198        }
1199        if (cv.containsKey(StructuredPostal.POSTCODE)) {
1200            s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
1201        }
1202        if (cv.containsKey(StructuredPostal.REGION)) {
1203            s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
1204        }
1205        if (cv.containsKey(StructuredPostal.STREET)) {
1206            s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
1207        }
1208    }
1209
1210    private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
1211        switch (cv.getAsInteger(StructuredPostal.TYPE)) {
1212            case StructuredPostal.TYPE_HOME:
1213                sendOnePostal(s, cv, new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
1214                        Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
1215                        Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
1216                        Tags.CONTACTS_HOME_ADDRESS_STATE,
1217                        Tags.CONTACTS_HOME_ADDRESS_STREET});
1218                break;
1219            case StructuredPostal.TYPE_WORK:
1220                sendOnePostal(s, cv, new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
1221                        Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
1222                        Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
1223                        Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
1224                        Tags.CONTACTS_BUSINESS_ADDRESS_STREET});
1225                break;
1226            case StructuredPostal.TYPE_OTHER:
1227                sendOnePostal(s, cv, new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
1228                        Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
1229                        Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
1230                        Tags.CONTACTS_OTHER_ADDRESS_STATE,
1231                        Tags.CONTACTS_OTHER_ADDRESS_STREET});
1232                break;
1233            default:
1234                break;
1235        }
1236    }
1237
1238    private void sendStructuredName(Serializer s, ContentValues cv) throws IOException {
1239        if (cv.containsKey(StructuredName.FAMILY_NAME)) {
1240            s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME));
1241        }
1242        if (cv.containsKey(StructuredName.GIVEN_NAME)) {
1243            s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME));
1244        }
1245        if (cv.containsKey(StructuredName.MIDDLE_NAME)) {
1246            s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME));
1247        }
1248        if (cv.containsKey(StructuredName.SUFFIX)) {
1249            s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX));
1250        }
1251    }
1252
1253    private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
1254        if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) {
1255            s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME));
1256        }
1257        if (cv.containsKey(EasBusiness.ASSISTANT_NAME)) {
1258            s.data(Tags.CONTACTS_ASSISTANT_NAME, cv.getAsString(EasBusiness.ASSISTANT_NAME));
1259        }
1260        if (cv.containsKey(EasBusiness.CUSTOMER_ID)) {
1261            s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID));
1262        }
1263        if (cv.containsKey(EasBusiness.DEPARTMENT)) {
1264            s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(EasBusiness.DEPARTMENT));
1265        }
1266        if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) {
1267            s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID));
1268        }
1269        if (cv.containsKey(EasBusiness.MANAGER_NAME)) {
1270            s.data(Tags.CONTACTS2_MANAGER_NAME, cv.getAsString(EasBusiness.MANAGER_NAME));
1271        }
1272        if (cv.containsKey(EasBusiness.OFFICE_LOCATION)) {
1273            s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(EasBusiness.OFFICE_LOCATION));
1274        }
1275    }
1276
1277    private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
1278        if (cv.containsKey(EasPersonal.ANNIVERSARY)) {
1279            s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY));
1280        }
1281        if (cv.containsKey(EasPersonal.BIRTHDAY)) {
1282            s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(EasPersonal.BIRTHDAY));
1283        }
1284        if (cv.containsKey(EasPersonal.FILE_AS)) {
1285            s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS));
1286        }
1287        if (cv.containsKey(EasPersonal.SPOUSE)) {
1288            s.data(Tags.CONTACTS_SPOUSE, cv.getAsString(EasPersonal.SPOUSE));
1289        }
1290        if (cv.containsKey(EasPersonal.TITLE)) {
1291            s.data(Tags.CONTACTS_TITLE, cv.getAsString(EasPersonal.TITLE));
1292        }
1293        if (cv.containsKey(EasPersonal.WEBPAGE)) {
1294            s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(EasPersonal.WEBPAGE));
1295        }
1296    }
1297
1298    private void sendYomi(Serializer s, ContentValues cv) throws IOException {
1299        if (cv.containsKey(Yomi.FIRST_NAME)) {
1300            s.data(Tags.CONTACTS_YOMI_FIRST_NAME, cv.getAsString(Yomi.FIRST_NAME));
1301        }
1302        if (cv.containsKey(Yomi.LAST_NAME)) {
1303            s.data(Tags.CONTACTS_YOMI_LAST_NAME, cv.getAsString(Yomi.LAST_NAME));
1304        }
1305        if (cv.containsKey(Yomi.COMPANY_NAME)) {
1306            s.data(Tags.CONTACTS_YOMI_COMPANY_NAME, cv.getAsString(Yomi.COMPANY_NAME));
1307        }
1308    }
1309
1310    private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
1311        if (cv.containsKey(Organization.TITLE)) {
1312            s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
1313        }
1314        if (cv.containsKey(Organization.COMPANY)) {
1315            s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY));
1316        }
1317    }
1318
1319    private void sendNickname(Serializer s, ContentValues cv) throws IOException {
1320        if (cv.containsKey(Nickname.NAME)) {
1321            s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME));
1322        }
1323    }
1324
1325    private void sendNote(Serializer s, ContentValues cv) throws IOException {
1326        if (cv.containsKey(Note.NOTE)) {
1327            // EAS won't accept note data with raw newline characters
1328            String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
1329            // Format of upsync data depends on protocol version
1330            if (mService.mProtocolVersionDouble >= 12.0) {
1331                s.start(Tags.BASE_BODY);
1332                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
1333                s.end();
1334            } else {
1335                s.data(Tags.CONTACTS_BODY, note);
1336            }
1337        }
1338    }
1339
1340    private void sendChildren(Serializer s, ContentValues cv) throws IOException {
1341        boolean first = true;
1342        for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
1343            String row = EasChildren.ROWS[i];
1344            if (cv.containsKey(row)) {
1345                if (first) {
1346                    s.start(Tags.CONTACTS_CHILDREN);
1347                    first = false;
1348                }
1349                s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
1350            }
1351        }
1352        if (!first) {
1353            s.end();
1354        }
1355    }
1356
1357    private void sendPhone(Serializer s, ContentValues cv) throws IOException {
1358        String value = cv.getAsString(Phone.NUMBER);
1359        switch (cv.getAsInteger(Phone.TYPE)) {
1360            case TYPE_WORK2:
1361                s.data(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER, value);
1362                break;
1363            case Phone.TYPE_WORK:
1364                s.data(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER, value);
1365                break;
1366            case TYPE_MMS:
1367                s.data(Tags.CONTACTS2_MMS, value);
1368                break;
1369            case Phone.TYPE_FAX_WORK:
1370                s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
1371                break;
1372            case TYPE_COMPANY_MAIN:
1373                s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
1374                break;
1375            case Phone.TYPE_HOME:
1376                s.data(Tags.CONTACTS_HOME_TELEPHONE_NUMBER, value);
1377                break;
1378            case TYPE_HOME2:
1379                s.data(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER, value);
1380                break;
1381            case Phone.TYPE_MOBILE:
1382                s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
1383                break;
1384            case TYPE_CAR:
1385                s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
1386                break;
1387            case Phone.TYPE_PAGER:
1388                s.data(Tags.CONTACTS_PAGER_NUMBER, value);
1389                break;
1390            case TYPE_RADIO:
1391                s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
1392                break;
1393            case Phone.TYPE_FAX_HOME:
1394                s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
1395                break;
1396            case TYPE_EMAIL2:
1397                s.data(Tags.CONTACTS_EMAIL2_ADDRESS, value);
1398                break;
1399            case TYPE_EMAIL3:
1400                s.data(Tags.CONTACTS_EMAIL3_ADDRESS, value);
1401                break;
1402            default:
1403                break;
1404        }
1405    }
1406
1407    @Override
1408    public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException {
1409        // First, let's find Contacts that have changed.
1410        ContentResolver cr = service.mContentResolver;
1411        Uri uri = RawContacts.CONTENT_URI.buildUpon()
1412                .appendQueryParameter(RawContacts.ACCOUNT_NAME, service.mAccount.mEmailAddress)
1413                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
1414                .build();
1415
1416        if (service.mMailbox.mSyncKey.equals("0")) {
1417            return false;
1418        }
1419
1420        try {
1421            // Get them all atomically
1422            EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
1423            try {
1424                boolean first = true;
1425                while (ei.hasNext()) {
1426                    Entity entity = ei.next();
1427                    // For each of these entities, create the change commands
1428                    ContentValues entityValues = entity.getEntityValues();
1429                    String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
1430                    ArrayList<Integer> groupIds = new ArrayList<Integer>();
1431                    if (first) {
1432                        s.start(Tags.SYNC_COMMANDS);
1433                        first = false;
1434                    }
1435                    s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId)
1436                        .start(Tags.SYNC_APPLICATION_DATA);
1437                    // Write out the data here
1438                    for (NamedContentValues ncv: entity.getSubValues()) {
1439                        ContentValues cv = ncv.values;
1440                        String mimeType = cv.getAsString(Data.MIMETYPE);
1441                        if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
1442                            sendEmail(s, cv);
1443                        } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
1444                            sendNickname(s, cv);
1445                        } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
1446                            sendChildren(s, cv);
1447                        } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
1448                            sendBusiness(s, cv);
1449                        } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
1450                            sendPersonal(s, cv);
1451                        } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
1452                            sendPhone(s, cv);
1453                        } else if (mimeType.equals(Yomi.CONTENT_ITEM_TYPE)) {
1454                            sendYomi(s, cv);
1455                        } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
1456                            sendStructuredName(s, cv);
1457                        } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
1458                            sendStructuredPostal(s, cv);
1459                        } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
1460                            sendOrganization(s, cv);
1461                        } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
1462                            sendIm(s, cv);
1463                        } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
1464                            // We must gather these, and send them together (below)
1465                            groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
1466                        } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
1467                            sendNote(s, cv);
1468                        } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
1469                            // For now, the user can change the photo, but the change won't be
1470                            // uploaded.
1471                        } else {
1472                            userLog("Contacts upsync, unknown data: ", mimeType);
1473                        }
1474                    }
1475
1476                    // Now, we'll send up groups, if any
1477                    if (!groupIds.isEmpty()) {
1478                        boolean groupFirst = true;
1479                        for (int id: groupIds) {
1480                            // Since we get id's from the provider, we need to find their names
1481                            Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
1482                                    GROUP_PROJECTION, null, null, null);
1483                            try {
1484                                // Presumably, this should always succeed, but ...
1485                                if (c.moveToFirst()) {
1486                                    if (groupFirst) {
1487                                        s.start(Tags.CONTACTS_CATEGORIES);
1488                                        groupFirst = false;
1489                                    }
1490                                    s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
1491                                }
1492                            } finally {
1493                                c.close();
1494                            }
1495                        }
1496                        if (!groupFirst) {
1497                            s.end();
1498                        }
1499                    }
1500                    s.end().end(); // ApplicationData & Change
1501                    mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
1502                }
1503                if (!first) {
1504                    s.end(); // Commands
1505                }
1506            } finally {
1507                ei.close();
1508            }
1509        } catch (RemoteException e) {
1510            Log.e(TAG, "Could not read dirty contacts.");
1511        }
1512
1513        return false;
1514    }
1515}
1516