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