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