• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.exchange.adapter;
2 
3 import android.content.ContentProviderOperation;
4 import android.content.ContentProviderOperation.Builder;
5 import android.content.ContentProviderResult;
6 import android.content.ContentResolver;
7 import android.content.ContentUris;
8 import android.content.ContentValues;
9 import android.content.Context;
10 import android.content.Entity;
11 import android.content.Entity.NamedContentValues;
12 import android.content.EntityIterator;
13 import android.content.OperationApplicationException;
14 import android.database.Cursor;
15 import android.net.Uri;
16 import android.os.RemoteException;
17 import android.provider.ContactsContract;
18 import android.provider.ContactsContract.CommonDataKinds.Email;
19 import android.provider.ContactsContract.CommonDataKinds.Event;
20 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
21 import android.provider.ContactsContract.CommonDataKinds.Im;
22 import android.provider.ContactsContract.CommonDataKinds.Nickname;
23 import android.provider.ContactsContract.CommonDataKinds.Note;
24 import android.provider.ContactsContract.CommonDataKinds.Organization;
25 import android.provider.ContactsContract.CommonDataKinds.Phone;
26 import android.provider.ContactsContract.CommonDataKinds.Photo;
27 import android.provider.ContactsContract.CommonDataKinds.Relation;
28 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
29 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
30 import android.provider.ContactsContract.CommonDataKinds.Website;
31 import android.provider.ContactsContract.Data;
32 import android.provider.ContactsContract.RawContacts;
33 import android.provider.ContactsContract.SyncState;
34 import android.provider.SyncStateContract;
35 import android.text.TextUtils;
36 import android.text.util.Rfc822Token;
37 import android.text.util.Rfc822Tokenizer;
38 import android.util.Base64;
39 
40 import com.android.emailcommon.provider.Account;
41 import com.android.emailcommon.provider.Mailbox;
42 import com.android.emailcommon.utility.Utility;
43 import com.android.exchange.Eas;
44 import com.android.exchange.eas.EasSyncCollectionTypeBase;
45 import com.android.exchange.eas.EasSyncContacts;
46 import com.android.exchange.utility.CalendarUtilities;
47 import com.android.mail.utils.LogUtils;
48 
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.text.ParseException;
52 import java.util.ArrayList;
53 import java.util.GregorianCalendar;
54 import java.util.TimeZone;
55 
56 public class ContactsSyncParser extends AbstractSyncParser {
57     private static final String TAG = Eas.LOG_TAG;
58 
59     private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
60     private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
61     private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
62 
63     private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
64         = new ArrayList<NamedContentValues>();
65 
66     private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
67 
68     private static final int MAX_IM_ROWS = 3;
69     private static final int MAX_EMAIL_ROWS = 3;
70     private static final int MAX_PHONE_ROWS = 2;
71     private static final String COMMON_DATA_ROW = Im.DATA;  // Could have been Email.DATA, etc.
72     private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
73 
74     String[] mBindArgument = new String[1];
75     ContactOperations ops = new ContactOperations();
76     private final android.accounts.Account mAccountManagerAccount;
77     private final Uri mAccountUri;
78     private boolean mGroupsUsed = false;
79 
ContactsSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Mailbox mailbox, final Account account, final android.accounts.Account accountManagerAccount)80     public ContactsSyncParser(final Context context, final ContentResolver resolver,
81             final InputStream in, final Mailbox mailbox, final Account account,
82             final android.accounts.Account accountManagerAccount) throws IOException {
83         super(context, resolver, in, mailbox, account);
84         mAccountManagerAccount = accountManagerAccount;
85         mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI,
86                 mAccount.mEmailAddress);
87     }
88 
isGroupsUsed()89     public boolean isGroupsUsed() {
90         return mGroupsUsed;
91     }
92 
addData(String serverId, ContactOperations ops, Entity entity)93     public void addData(String serverId, ContactOperations ops, Entity entity)
94             throws IOException {
95         String prefix = null;
96         String firstName = null;
97         String lastName = null;
98         String middleName = null;
99         String suffix = null;
100         String companyName = null;
101         String yomiFirstName = null;
102         String yomiLastName = null;
103         String yomiCompanyName = null;
104         String title = null;
105         String department = null;
106         String officeLocation = null;
107         Address home = new Address();
108         Address work = new Address();
109         Address other = new Address();
110         EasBusiness business = new EasBusiness();
111         EasPersonal personal = new EasPersonal();
112         ArrayList<String> children = new ArrayList<String>();
113         ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
114         ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
115         ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
116         ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
117         if (entity == null) {
118             ops.newContact(serverId, mAccount.mEmailAddress);
119         }
120 
121         while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
122             switch (tag) {
123                 case Tags.CONTACTS_FIRST_NAME:
124                     firstName = getValue();
125                     break;
126                 case Tags.CONTACTS_LAST_NAME:
127                     lastName = getValue();
128                     break;
129                 case Tags.CONTACTS_MIDDLE_NAME:
130                     middleName = getValue();
131                     break;
132                 case Tags.CONTACTS_SUFFIX:
133                     suffix = getValue();
134                     break;
135                 case Tags.CONTACTS_COMPANY_NAME:
136                     companyName = getValue();
137                     break;
138                 case Tags.CONTACTS_JOB_TITLE:
139                     title = getValue();
140                     break;
141                 case Tags.CONTACTS_EMAIL1_ADDRESS:
142                 case Tags.CONTACTS_EMAIL2_ADDRESS:
143                 case Tags.CONTACTS_EMAIL3_ADDRESS:
144                     emails.add(new EmailRow(getValue()));
145                     break;
146                 case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
147                 case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
148                     workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
149                     break;
150                 case Tags.CONTACTS2_MMS:
151                     ops.addPhone(entity, Phone.TYPE_MMS, getValue());
152                     break;
153                 case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
154                     ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
155                     break;
156                 case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
157                     ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
158                     break;
159                 case Tags.CONTACTS_HOME_FAX_NUMBER:
160                     ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
161                     break;
162                 case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
163                 case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
164                     homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
165                     break;
166                 case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
167                     ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
168                     break;
169                 case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
170                     ops.addPhone(entity, Phone.TYPE_CAR, getValue());
171                     break;
172                 case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
173                     ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
174                     break;
175                 case Tags.CONTACTS_PAGER_NUMBER:
176                     ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
177                     break;
178                 case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
179                     ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
180                     break;
181                 case Tags.CONTACTS2_IM_ADDRESS:
182                 case Tags.CONTACTS2_IM_ADDRESS_2:
183                 case Tags.CONTACTS2_IM_ADDRESS_3:
184                     ims.add(new ImRow(getValue()));
185                     break;
186                 case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
187                     work.city = getValue();
188                     break;
189                 case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
190                     work.country = getValue();
191                     break;
192                 case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
193                     work.code = getValue();
194                     break;
195                 case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
196                     work.state = getValue();
197                     break;
198                 case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
199                     work.street = getValue();
200                     break;
201                 case Tags.CONTACTS_HOME_ADDRESS_CITY:
202                     home.city = getValue();
203                     break;
204                 case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
205                     home.country = getValue();
206                     break;
207                 case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
208                     home.code = getValue();
209                     break;
210                 case Tags.CONTACTS_HOME_ADDRESS_STATE:
211                     home.state = getValue();
212                     break;
213                 case Tags.CONTACTS_HOME_ADDRESS_STREET:
214                     home.street = getValue();
215                     break;
216                 case Tags.CONTACTS_OTHER_ADDRESS_CITY:
217                     other.city = getValue();
218                     break;
219                 case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
220                     other.country = getValue();
221                     break;
222                 case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
223                     other.code = getValue();
224                     break;
225                 case Tags.CONTACTS_OTHER_ADDRESS_STATE:
226                     other.state = getValue();
227                     break;
228                 case Tags.CONTACTS_OTHER_ADDRESS_STREET:
229                     other.street = getValue();
230                     break;
231 
232                 case Tags.CONTACTS_CHILDREN:
233                     childrenParser(children);
234                     break;
235 
236                 case Tags.CONTACTS_YOMI_COMPANY_NAME:
237                     yomiCompanyName = getValue();
238                     break;
239                 case Tags.CONTACTS_YOMI_FIRST_NAME:
240                     yomiFirstName = getValue();
241                     break;
242                 case Tags.CONTACTS_YOMI_LAST_NAME:
243                     yomiLastName = getValue();
244                     break;
245 
246                 case Tags.CONTACTS2_NICKNAME:
247                     ops.addNickname(entity, getValue());
248                     break;
249 
250                 case Tags.CONTACTS_ASSISTANT_NAME:
251                     ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
252                     break;
253                 case Tags.CONTACTS2_MANAGER_NAME:
254                     ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
255                     break;
256                 case Tags.CONTACTS_SPOUSE:
257                     ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
258                     break;
259                 case Tags.CONTACTS_DEPARTMENT:
260                     department = getValue();
261                     break;
262                 case Tags.CONTACTS_TITLE:
263                     prefix = getValue();
264                     break;
265 
266                 // EAS Business
267                 case Tags.CONTACTS_OFFICE_LOCATION:
268                     officeLocation = getValue();
269                     break;
270                 case Tags.CONTACTS2_CUSTOMER_ID:
271                     business.customerId = getValue();
272                     break;
273                 case Tags.CONTACTS2_GOVERNMENT_ID:
274                     business.governmentId = getValue();
275                     break;
276                 case Tags.CONTACTS2_ACCOUNT_NAME:
277                     business.accountName = getValue();
278                     break;
279 
280                 // EAS Personal
281                 case Tags.CONTACTS_ANNIVERSARY:
282                     personal.anniversary = getValue();
283                     break;
284                 case Tags.CONTACTS_FILE_AS:
285                     personal.fileAs = getValue();
286                     break;
287                 case Tags.CONTACTS_BIRTHDAY:
288                     ops.addBirthday(entity, getValue());
289                     break;
290                 case Tags.CONTACTS_WEBPAGE:
291                     ops.addWebpage(entity, getValue());
292                     break;
293 
294                 case Tags.CONTACTS_PICTURE:
295                     ops.addPhoto(entity, getValue());
296                     break;
297 
298                 case Tags.BASE_BODY:
299                     ops.addNote(entity, bodyParser());
300                     break;
301                 case Tags.CONTACTS_BODY:
302                     ops.addNote(entity, getValue());
303                     break;
304 
305                 case Tags.CONTACTS_CATEGORIES:
306                     mGroupsUsed = true;
307                     categoriesParser(ops, entity);
308                     break;
309 
310                 default:
311                     skipTag();
312             }
313         }
314 
315         ops.addName(entity, prefix, firstName, lastName, middleName, suffix,
316                 yomiFirstName, yomiLastName);
317         ops.addBusiness(entity, business);
318         ops.addPersonal(entity, personal);
319 
320         ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
321         ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
322         ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
323                 MAX_PHONE_ROWS);
324         ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
325                 MAX_PHONE_ROWS);
326 
327         if (!children.isEmpty()) {
328             ops.addChildren(entity, children);
329         }
330 
331         if (work.hasData()) {
332             ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
333                     work.state, work.country, work.code);
334         }
335         if (home.hasData()) {
336             ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
337                     home.state, home.country, home.code);
338         }
339         if (other.hasData()) {
340             ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
341                     other.state, other.country, other.code);
342         }
343 
344         if (companyName != null) {
345             ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
346                     yomiCompanyName, officeLocation);
347         }
348 
349         if (entity != null) {
350             // We've been removing rows from the list as they've been found in the xml
351             // Any that are left must have been deleted on the server
352             ArrayList<NamedContentValues> ncvList = entity.getSubValues();
353             for (NamedContentValues ncv: ncvList) {
354                 // These rows need to be deleted...
355                 Uri u = dataUriFromNamedContentValues(ncv);
356                 ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
357                         .build());
358             }
359         }
360     }
361 
categoriesParser(ContactOperations ops, Entity entity)362     private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
363         while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
364             switch (tag) {
365                 case Tags.CONTACTS_CATEGORY:
366                     ops.addGroup(entity, getValue());
367                     break;
368                 default:
369                     skipTag();
370             }
371         }
372     }
373 
childrenParser(ArrayList<String> children)374     private void childrenParser(ArrayList<String> children) throws IOException {
375         while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
376             switch (tag) {
377                 case Tags.CONTACTS_CHILD:
378                     if (children.size() < EasChildren.MAX_CHILDREN) {
379                         children.add(getValue());
380                     }
381                     break;
382                 default:
383                     skipTag();
384             }
385         }
386     }
387 
bodyParser()388     private String bodyParser() throws IOException {
389         String body = null;
390         while (nextTag(Tags.BASE_BODY) != END) {
391             switch (tag) {
392                 case Tags.BASE_DATA:
393                     body = getValue();
394                     break;
395                 default:
396                     skipTag();
397             }
398         }
399         return body;
400     }
401 
addParser(ContactOperations ops)402     public void addParser(ContactOperations ops) throws IOException {
403         String serverId = null;
404         while (nextTag(Tags.SYNC_ADD) != END) {
405             switch (tag) {
406                 case Tags.SYNC_SERVER_ID: // same as
407                     serverId = getValue();
408                     break;
409                 case Tags.SYNC_APPLICATION_DATA:
410                     addData(serverId, ops, null);
411                     break;
412                 default:
413                     skipTag();
414             }
415         }
416     }
417 
getServerIdCursor(String serverId)418     private Cursor getServerIdCursor(String serverId) {
419         mBindArgument[0] = serverId;
420         return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
421                 mBindArgument, null);
422     }
423 
getClientIdCursor(String clientId)424     private Cursor getClientIdCursor(String clientId) {
425         mBindArgument[0] = clientId;
426         return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
427                 mBindArgument, null);
428     }
429 
deleteParser(ContactOperations ops)430     public void deleteParser(ContactOperations ops) throws IOException {
431         while (nextTag(Tags.SYNC_DELETE) != END) {
432             switch (tag) {
433                 case Tags.SYNC_SERVER_ID:
434                     String serverId = getValue();
435                     // Find the message in this mailbox with the given serverId
436                     Cursor c = getServerIdCursor(serverId);
437                     try {
438                         if (c.moveToFirst()) {
439                             userLog("Deleting ", serverId);
440                             ops.delete(c.getLong(0));
441                         }
442                     } finally {
443                         c.close();
444                     }
445                     break;
446                 default:
447                     skipTag();
448             }
449         }
450     }
451 
452     class ServerChange {
453         long id;
454         boolean read;
455 
ServerChange(long _id, boolean _read)456         ServerChange(long _id, boolean _read) {
457             id = _id;
458             read = _read;
459         }
460     }
461 
462     /**
463      * Changes are handled row by row, and only changed/new rows are acted upon
464      * @param ops the array of pending ContactProviderOperations.
465      * @throws IOException
466      */
changeParser(ContactOperations ops)467     public void changeParser(ContactOperations ops) throws IOException {
468         String serverId = null;
469         Entity entity = null;
470         while (nextTag(Tags.SYNC_CHANGE) != END) {
471             switch (tag) {
472                 case Tags.SYNC_SERVER_ID:
473                     serverId = getValue();
474                     Cursor c = getServerIdCursor(serverId);
475                     try {
476                         if (c.moveToFirst()) {
477                             // TODO Handle deleted individual rows...
478                             Uri uri = ContentUris.withAppendedId(
479                                     RawContacts.CONTENT_URI, c.getLong(0));
480                             uri = Uri.withAppendedPath(
481                                     uri, RawContacts.Entity.CONTENT_DIRECTORY);
482                             final Cursor cursor = mContentResolver.query(uri,
483                                     null, null, null, null);
484                             if (cursor != null) {
485                                 final EntityIterator entityIterator =
486                                     RawContacts.newEntityIterator(cursor);
487                                 if (entityIterator.hasNext()) {
488                                     entity = entityIterator.next();
489                                 }
490                                 userLog("Changing contact ", serverId);
491                             }
492                         }
493                     } finally {
494                         c.close();
495                     }
496                     break;
497                 case Tags.SYNC_APPLICATION_DATA:
498                     addData(serverId, ops, entity);
499                     break;
500                 default:
501                     skipTag();
502             }
503         }
504     }
505 
506     @Override
commandsParser()507     public void commandsParser() throws IOException {
508         while (nextTag(Tags.SYNC_COMMANDS) != END) {
509             if (tag == Tags.SYNC_ADD) {
510                 addParser(ops);
511             } else if (tag == Tags.SYNC_DELETE) {
512                 deleteParser(ops);
513             } else if (tag == Tags.SYNC_CHANGE) {
514                 changeParser(ops);
515             } else
516                 skipTag();
517         }
518     }
519 
520     @Override
commit()521     public void commit() throws IOException {
522        // Save the syncKey here, using the Helper provider by Contacts provider
523         userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
524         ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
525                 mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
526 
527         // Execute these all at once...
528         ops.execute(mContext);
529 
530         if (ops.mResults != null && ops.mResults.length > 0) {
531             final ContentValues cv = new ContentValues();
532             cv.put(RawContacts.DIRTY, 0);
533             for (int i = 0; i < ops.mContactIndexCount; i++) {
534                 final int index = ops.mContactIndexArray[i];
535                 final Uri u = index < ops.mResults.length ? ops.mResults[index].uri : null;
536                 if (u != null) {
537                     String idString = u.getLastPathSegment();
538                     mContentResolver.update(
539                             addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
540                             RawContacts._ID + "=" + idString, null);
541                 }
542             }
543         }
544     }
545 
546     public void addResponsesParser() throws IOException {
547         String serverId = null;
548         String clientId = null;
549         ContentValues cv = new ContentValues();
550         while (nextTag(Tags.SYNC_ADD) != END) {
551             switch (tag) {
552                 case Tags.SYNC_SERVER_ID:
553                     serverId = getValue();
554                     break;
555                 case Tags.SYNC_CLIENT_ID:
556                     clientId = getValue();
557                     break;
558                 case Tags.SYNC_STATUS:
559                     getValue();
560                     break;
561                 default:
562                     skipTag();
563             }
564         }
565 
566         // This is theoretically impossible, but...
567         if (clientId == null || serverId == null) return;
568 
569         Cursor c = getClientIdCursor(clientId);
570         try {
571             if (c.moveToFirst()) {
572                 cv.put(RawContacts.SOURCE_ID, serverId);
573                 cv.put(RawContacts.DIRTY, 0);
574                 ops.add(ContentProviderOperation.newUpdate(
575                         ContentUris.withAppendedId(
576                                 addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
577                                 c.getLong(0)))
578                         .withValues(cv)
579                         .build());
580                 userLog("New contact " + clientId + " was given serverId: " + serverId);
581             }
582         } finally {
583             c.close();
584         }
585     }
586 
587     public void changeResponsesParser() throws IOException {
588         String serverId = null;
589         String status = null;
590         while (nextTag(Tags.SYNC_CHANGE) != END) {
591             switch (tag) {
592                 case Tags.SYNC_SERVER_ID:
593                     serverId = getValue();
594                     break;
595                 case Tags.SYNC_STATUS:
596                     status = getValue();
597                     break;
598                 default:
599                     skipTag();
600             }
601         }
602         if (serverId != null && status != null) {
603             userLog("Changed contact " + serverId + " failed with status: " + status);
604         }
605     }
606 
607 
608     @Override
609     public void responsesParser() throws IOException {
610         // Handle server responses here (for Add and Change)
611         while (nextTag(Tags.SYNC_RESPONSES) != END) {
612             if (tag == Tags.SYNC_ADD) {
613                 addResponsesParser();
614             } else if (tag == Tags.SYNC_CHANGE) {
615                 changeResponsesParser();
616             } else
617                 skipTag();
618         }
619     }
620 
621     private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) {
622         return uri.buildUpon()
623             .appendQueryParameter(RawContacts.ACCOUNT_NAME, emailAddress)
624             .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
625             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
626             .build();
627     }
628 
629     static Uri addCallerIsSyncAdapterParameter(Uri uri) {
630         return uri.buildUpon()
631                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
632                 .build();
633     }
634 
635     /**
636      * Generate the uri for the data row associated with this NamedContentValues object
637      * @param ncv the NamedContentValues object
638      * @return a uri that can be used to refer to this row
639      */
640     public static Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
641         long id = ncv.values.getAsLong(RawContacts._ID);
642         Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
643         return dataUri;
644     }
645 
646     public static final class EasChildren {
647         private EasChildren() {}
648 
649         /** MIME type used when storing this in data table. */
650         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
651         public static final int MAX_CHILDREN = 8;
652         public static final String[] ROWS =
653             new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
654     }
655 
656     public static final class EasPersonal {
657         String anniversary;
658         String fileAs;
659 
660             /** MIME type used when storing this in data table. */
661         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
662         public static final String ANNIVERSARY = "data2";
663         public static final String FILE_AS = "data4";
664 
665         boolean hasData() {
666             return anniversary != null || fileAs != null;
667         }
668     }
669 
670     public static final class EasBusiness {
671         String customerId;
672         String governmentId;
673         String accountName;
674 
675         /** MIME type used when storing this in data table. */
676         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
677         public static final String CUSTOMER_ID = "data6";
678         public static final String GOVERNMENT_ID = "data7";
679         public static final String ACCOUNT_NAME = "data8";
680 
681         boolean hasData() {
682             return customerId != null || governmentId != null || accountName != null;
683         }
684     }
685 
686     public static final class Address {
687         String city;
688         String country;
689         String code;
690         String street;
691         String state;
692 
693         boolean hasData() {
694             return city != null || country != null || code != null || state != null
695                 || street != null;
696         }
697     }
698 
699     interface UntypedRow {
700         public void addValues(RowBuilder builder);
701         public boolean isSameAs(int type, String value);
702     }
703 
704     static class EmailRow implements UntypedRow {
705         String email;
706         String displayName;
707 
708         public EmailRow(String _email) {
709             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
710             // Can't happen, but belt & suspenders
711             if (tokens.length == 0) {
712                 email = "";
713                 displayName = "";
714             } else {
715                 Rfc822Token token = tokens[0];
716                 email = token.getAddress();
717                 displayName = token.getName();
718             }
719         }
720 
721         @Override
722         public void addValues(RowBuilder builder) {
723             builder.withValue(Email.DATA, email);
724             builder.withValue(Email.DISPLAY_NAME, displayName);
725         }
726 
727         @Override
728         public boolean isSameAs(int type, String value) {
729             return email.equalsIgnoreCase(value);
730         }
731     }
732 
733     static class ImRow implements UntypedRow {
734         String im;
735 
736         public ImRow(String _im) {
737             im = _im;
738         }
739 
740         @Override
741         public void addValues(RowBuilder builder) {
742             builder.withValue(Im.DATA, im);
743         }
744 
745         @Override
746         public boolean isSameAs(int type, String value) {
747             return im.equalsIgnoreCase(value);
748         }
749     }
750 
751     static class PhoneRow implements UntypedRow {
752         String phone;
753         int type;
754 
755         public PhoneRow(String _phone, int _type) {
756             phone = _phone;
757             type = _type;
758         }
759 
760         @Override
761         public void addValues(RowBuilder builder) {
762             builder.withValue(Im.DATA, phone);
763             builder.withValue(Phone.TYPE, type);
764         }
765 
766         @Override
767         public boolean isSameAs(int _type, String value) {
768             return type == _type && phone.equalsIgnoreCase(value);
769         }
770     }
771 
772     /**
773      * RowBuilder is a wrapper for the Builder class that is used to create/update rows for a
774      * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
775      * represent the current values of that row, that can be compared against current values to
776      * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
777      * the Builder.
778      */
779     private static class RowBuilder {
780         Builder builder;
781         ContentValues cv;
782 
783         public RowBuilder(Builder _builder) {
784             builder = _builder;
785         }
786 
787         public RowBuilder(Builder _builder, NamedContentValues _ncv) {
788             builder = _builder;
789             cv = _ncv.values;
790         }
791 
792         RowBuilder withValueBackReference(String key, int previousResult) {
793             builder.withValueBackReference(key, previousResult);
794             return this;
795         }
796 
797         ContentProviderOperation build() {
798             return builder.build();
799         }
800 
801         RowBuilder withValue(String key, Object value) {
802             builder.withValue(key, value);
803             return this;
804         }
805     }
806     public static class ContactOperations extends ArrayList<ContentProviderOperation> {
807         private static final long serialVersionUID = 1L;
808         private int mCount = 0;
809         private int mContactBackValue = mCount;
810         // Make an array big enough for the max possible window size.
811         private final int[] mContactIndexArray = new int[EasSyncCollectionTypeBase.MAX_WINDOW_SIZE];
812         private int mContactIndexCount = 0;
813         private ContentProviderResult[] mResults = null;
814 
815         @Override
816         public boolean add(ContentProviderOperation op) {
817             super.add(op);
818             mCount++;
819             return true;
820         }
821 
822         public void newContact(final String serverId, final String emailAddress) {
823             Builder builder = ContentProviderOperation.newInsert(
824                     uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI, emailAddress));
825             ContentValues values = new ContentValues();
826             values.put(RawContacts.SOURCE_ID, serverId);
827             builder.withValues(values);
828             mContactBackValue = mCount;
829             mContactIndexArray[mContactIndexCount++] = mCount;
830             add(builder.build());
831         }
832 
833         public void delete(long id) {
834             add(ContentProviderOperation
835                     .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
836                             .buildUpon()
837                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
838                             .build())
839                     .build());
840         }
841 
842         public void execute(final Context context) {
843             try {
844                 if (!isEmpty()) {
845                     mResults = context.getContentResolver().applyBatch(
846                             ContactsContract.AUTHORITY, this);
847                 }
848             } catch (RemoteException e) {
849                 // There is nothing sensible to be done here
850                 LogUtils.e(TAG, "problem inserting contact during server update", e);
851             } catch (OperationApplicationException e) {
852                 // There is nothing sensible to be done here
853                 LogUtils.e(TAG, "problem inserting contact during server update", e);
854             } catch (IllegalArgumentException e) {
855                 // CP2 has been disabled
856                 LogUtils.e(TAG, "CP2 is disabled; unable to insert contact.");
857             }
858         }
859 
860         /**
861          * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
862          * tries to find a match, returning it
863          * @param list the list of NCV's from the contact entity
864          * @param contentItemType the mime type we're looking for
865          * @param type the subtype (e.g. HOME, WORK, etc.)
866          * @return the matching NCV or null if not found
867          */
868         private static NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
869                 String contentItemType, int type, String stringType) {
870             NamedContentValues result = null;
871             if (contentItemType == null) {
872                 return result;
873             }
874 
875             // Loop through the ncv's, looking for an existing row
876             for (NamedContentValues namedContentValues: list) {
877                 final Uri uri = namedContentValues.uri;
878                 final ContentValues cv = namedContentValues.values;
879                 if (Data.CONTENT_URI.equals(uri)) {
880                     final String mimeType = cv.getAsString(Data.MIMETYPE);
881                     if (TextUtils.equals(mimeType, contentItemType)) {
882                         if (stringType != null) {
883                             if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
884                                 result = namedContentValues;
885                             }
886                         // Note Email.TYPE could be ANY type column; they are all defined in
887                         // the private CommonColumns class in ContactsContract
888                         // We'll accept either type < 0 (don't care), cv doesn't have a type,
889                         // or the types are equal
890                         } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
891                                 cv.getAsInteger(Email.TYPE) == type) {
892                             result = namedContentValues;
893                         }
894                     }
895                 }
896             }
897 
898             // If we've found an existing data row, we'll delete it.  Any rows left at the
899             // end should be deleted...
900             if (result != null) {
901                 list.remove(result);
902             }
903 
904             // Return the row found (or null)
905             return result;
906         }
907 
908         /**
909          * Given the list of NamedContentValues for an entity and a mime type
910          * gather all of the matching NCV's, returning them
911          * @param list the list of NCV's from the contact entity
912          * @param contentItemType the mime type we're looking for
913          * @param type the subtype (e.g. HOME, WORK, etc.)
914          * @return the matching NCVs
915          */
916         private static ArrayList<NamedContentValues> findUntypedData(
917                 ArrayList<NamedContentValues> list, int type, String contentItemType) {
918             final ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
919             if (contentItemType == null) {
920                 return result;
921             }
922 
923             // Loop through the ncv's, looking for an existing row
924             for (NamedContentValues namedContentValues: list) {
925                 final Uri uri = namedContentValues.uri;
926                 final ContentValues cv = namedContentValues.values;
927                 if (Data.CONTENT_URI.equals(uri)) {
928                     final String mimeType = cv.getAsString(Data.MIMETYPE);
929                     if (TextUtils.equals(mimeType, contentItemType)) {
930                         if (type != -1) {
931                             final int subtype = cv.getAsInteger(Phone.TYPE);
932                             if (type != subtype) {
933                                 continue;
934                             }
935                         }
936                         result.add(namedContentValues);
937                     }
938                 }
939             }
940 
941             // If we've found an existing data row, we'll delete it.  Any rows left at the
942             // end should be deleted...
943             for (NamedContentValues values : result) {
944                 list.remove(values);
945             }
946 
947             // Return the row found (or null)
948             return result;
949         }
950 
951         /**
952          * Create a wrapper for a builder (insert or update) that also includes the NCV for
953          * an existing row of this type.   If the SmartBuilder's cv field is not null, then
954          * it represents the current (old) values of this field.  The caller can then check
955          * whether the field is now different and needs to be updated; if it's not different,
956          * the caller will simply return and not generate a new CPO.  Otherwise, the builder
957          * should have its content values set, and the built CPO should be added to the
958          * ContactOperations list.
959          *
960          * @param entity the contact entity (or null if this is a new contact)
961          * @param mimeType the mime type of this row
962          * @param type the subtype of this row
963          * @param stringType for groups, the name of the group (type will be ignored), or null
964          * @return the created SmartBuilder
965          */
966         public RowBuilder createBuilder(Entity entity, String mimeType, int type,
967                 String stringType) {
968             RowBuilder builder = null;
969 
970             if (entity != null) {
971                 NamedContentValues ncv =
972                     findTypedData(entity.getSubValues(), mimeType, type, stringType);
973                 if (ncv != null) {
974                     builder = new RowBuilder(
975                             ContentProviderOperation
976                                 .newUpdate(addCallerIsSyncAdapterParameter(
977                                     dataUriFromNamedContentValues(ncv))),
978                             ncv);
979                 }
980             }
981 
982             if (builder == null) {
983                 builder = newRowBuilder(entity, mimeType);
984             }
985 
986             // Return the appropriate builder (insert or update)
987             // Caller will fill in the appropriate values; 4 MIMETYPE is already set
988             return builder;
989         }
990 
991         private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
992             return createBuilder(entity, mimeType, type, null);
993         }
994 
995         private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
996             return createBuilder(entity, mimeType, -1, null);
997         }
998 
999         private RowBuilder newRowBuilder(Entity entity, String mimeType) {
1000             // This is a new row; first get the contactId
1001             // If the Contact is new, use the saved back value; otherwise the value in the entity
1002             int contactId = mContactBackValue;
1003             if (entity != null) {
1004                 contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
1005             }
1006 
1007             // Create an insert operation with the proper contactId reference
1008             RowBuilder builder =
1009                 new RowBuilder(ContentProviderOperation.newInsert(
1010                         addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
1011             if (entity == null) {
1012                 builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
1013             } else {
1014                 builder.withValue(Data.RAW_CONTACT_ID, contactId);
1015             }
1016 
1017             // Set the mime type of the row
1018             builder.withValue(Data.MIMETYPE, mimeType);
1019             return builder;
1020         }
1021 
1022         /**
1023          * Compare a column in a ContentValues with an (old) value, and see if they are the
1024          * same.  For this purpose, null and an empty string are considered the same.
1025          * @param cv a ContentValues object, from a NamedContentValues
1026          * @param column a column that might be in the ContentValues
1027          * @param oldValue an old value (or null) to check against
1028          * @return whether the column's value in the ContentValues matches oldValue
1029          */
1030         private static boolean cvCompareString(ContentValues cv, String column, String oldValue) {
1031             if (cv.containsKey(column)) {
1032                 if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
1033                     return true;
1034                 }
1035             } else if (oldValue == null || oldValue.length() == 0) {
1036                 return true;
1037             }
1038             return false;
1039         }
1040 
1041         public void addChildren(Entity entity, ArrayList<String> children) {
1042             RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
1043             int i = 0;
1044             for (String child: children) {
1045                 builder.withValue(EasChildren.ROWS[i++], child);
1046             }
1047             add(builder.build());
1048         }
1049 
1050         public void addGroup(Entity entity, String group) {
1051             RowBuilder builder =
1052                 createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
1053             builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
1054             add(builder.build());
1055         }
1056 
1057         public void addBirthday(Entity entity, String birthday) {
1058             RowBuilder builder =
1059                     typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
1060             ContentValues cv = builder.cv;
1061             if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
1062                 return;
1063             }
1064             // TODO: Store the date in the format expected by EAS servers.
1065             final long millis;
1066             try {
1067                 millis = Utility.parseEmailDateTimeToMillis(birthday);
1068             } catch (ParseException e) {
1069                 LogUtils.w(TAG, "Parse error for birthday date field.", e);
1070                 return;
1071             }
1072             GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
1073             cal.setTimeInMillis(millis);
1074             if (cal.get(GregorianCalendar.HOUR_OF_DAY) >= 12) {
1075                 cal.add(GregorianCalendar.DATE, 1);
1076             }
1077             String realBirthday = CalendarUtilities.calendarToBirthdayString(cal);
1078             builder.withValue(Event.START_DATE, realBirthday);
1079             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1080             add(builder.build());
1081         }
1082 
1083         public void addName(Entity entity, String prefix, String givenName, String familyName,
1084                 String middleName, String suffix, String yomiFirstName, String yomiLastName) {
1085             RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
1086             ContentValues cv = builder.cv;
1087             if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
1088                     cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
1089                     cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
1090                     cvCompareString(cv, StructuredName.PREFIX, prefix) &&
1091                     cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
1092                     cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
1093                     cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
1094                 return;
1095             }
1096             builder.withValue(StructuredName.GIVEN_NAME, givenName);
1097             builder.withValue(StructuredName.FAMILY_NAME, familyName);
1098             builder.withValue(StructuredName.MIDDLE_NAME, middleName);
1099             builder.withValue(StructuredName.SUFFIX, suffix);
1100             builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
1101             builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
1102             builder.withValue(StructuredName.PREFIX, prefix);
1103             add(builder.build());
1104         }
1105 
1106         public void addPersonal(Entity entity, EasPersonal personal) {
1107             RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
1108             ContentValues cv = builder.cv;
1109             if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
1110                     cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
1111                 return;
1112             }
1113             if (!personal.hasData()) {
1114                 return;
1115             }
1116             builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
1117             builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
1118             add(builder.build());
1119         }
1120 
1121         public void addBusiness(Entity entity, EasBusiness business) {
1122             RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
1123             ContentValues cv = builder.cv;
1124             if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
1125                     cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
1126                     cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
1127                 return;
1128             }
1129             if (!business.hasData()) {
1130                 return;
1131             }
1132             builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
1133             builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
1134             builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
1135             add(builder.build());
1136         }
1137 
1138         public void addPhoto(Entity entity, String photo) {
1139             // We're always going to add this; it's not worth trying to figure out whether the
1140             // picture is the same as the one stored.
1141             final byte[] pic;
1142             try {
1143                 pic = Base64.decode(photo, Base64.DEFAULT);
1144             } catch (IllegalArgumentException e) {
1145                 LogUtils.w(TAG, "Bad base-64 encoding; unable to decode photo.");
1146                 return;
1147             }
1148 
1149             final RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
1150             builder.withValue(Photo.PHOTO, pic);
1151             add(builder.build());
1152         }
1153 
1154         public void addPhone(Entity entity, int type, String phone) {
1155             RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
1156             ContentValues cv = builder.cv;
1157             if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
1158                 return;
1159             }
1160             builder.withValue(Phone.TYPE, type);
1161             builder.withValue(Phone.NUMBER, phone);
1162             add(builder.build());
1163         }
1164 
1165         public void addWebpage(Entity entity, String url) {
1166             RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
1167             ContentValues cv = builder.cv;
1168             if (cv != null && cvCompareString(cv, Website.URL, url)) {
1169                 return;
1170             }
1171             builder.withValue(Website.TYPE, Website.TYPE_WORK);
1172             builder.withValue(Website.URL, url);
1173             add(builder.build());
1174         }
1175 
1176         public void addRelation(Entity entity, int type, String value) {
1177             RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
1178             ContentValues cv = builder.cv;
1179             if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
1180                 return;
1181             }
1182             builder.withValue(Relation.TYPE, type);
1183             builder.withValue(Relation.DATA, value);
1184             add(builder.build());
1185         }
1186 
1187         public void addNickname(Entity entity, String name) {
1188             RowBuilder builder =
1189                 typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
1190             ContentValues cv = builder.cv;
1191             if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
1192                 return;
1193             }
1194             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1195             builder.withValue(Nickname.NAME, name);
1196             add(builder.build());
1197         }
1198 
1199         public void addPostal(Entity entity, int type, String street, String city, String state,
1200                 String country, String code) {
1201             RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
1202                     type);
1203             ContentValues cv = builder.cv;
1204             if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
1205                     cvCompareString(cv, StructuredPostal.STREET, street) &&
1206                     cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
1207                     cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
1208                     cvCompareString(cv, StructuredPostal.REGION, state)) {
1209                 return;
1210             }
1211             builder.withValue(StructuredPostal.TYPE, type);
1212             builder.withValue(StructuredPostal.CITY, city);
1213             builder.withValue(StructuredPostal.STREET, street);
1214             builder.withValue(StructuredPostal.COUNTRY, country);
1215             builder.withValue(StructuredPostal.POSTCODE, code);
1216             builder.withValue(StructuredPostal.REGION, state);
1217             add(builder.build());
1218         }
1219 
1220        /**
1221          * We now are dealing with up to maxRows typeless rows of mimeType data.  We need to try to
1222          * match them with existing rows; if there's a match, everything's great.  Otherwise, we
1223          * either need to add a new row for the data, or we have to replace an existing one
1224          * that no longer matches.  This is similar to the way Emails are handled.
1225          */
1226         public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
1227                 int type, int maxRows) {
1228             // Make a list of all same type rows in the existing entity
1229             ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
1230             ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
1231             if (entity != null) {
1232                 oldValues = findUntypedData(entityValues, type, mimeType);
1233                 entityValues = entity.getSubValues();
1234             }
1235 
1236             // These will be rows needing replacement with new values
1237             ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
1238 
1239             // The count of existing rows
1240             int numRows = oldValues.size();
1241             for (UntypedRow row: rows) {
1242                 boolean found = false;
1243                 // If we already have this row, mark it
1244                 for (NamedContentValues ncv: oldValues) {
1245                     ContentValues cv = ncv.values;
1246                     String data = cv.getAsString(COMMON_DATA_ROW);
1247                     int rowType = -1;
1248                     if (cv.containsKey(COMMON_TYPE_ROW)) {
1249                         rowType = cv.getAsInteger(COMMON_TYPE_ROW);
1250                     }
1251                     if (row.isSameAs(rowType, data)) {
1252                         cv.put(FOUND_DATA_ROW, true);
1253                         // Remove this to indicate it's still being used
1254                         entityValues.remove(ncv);
1255                         found = true;
1256                         break;
1257                     }
1258                 }
1259                 if (!found) {
1260                     // If we don't, there are two possibilities
1261                     if (numRows < maxRows) {
1262                         // If there are available rows, add a new one
1263                         RowBuilder builder = newRowBuilder(entity, mimeType);
1264                         row.addValues(builder);
1265                         add(builder.build());
1266                         numRows++;
1267                     } else {
1268                         // Otherwise, say we need to replace a row with this
1269                         rowsToReplace.add(row);
1270                     }
1271                 }
1272             }
1273 
1274             // Go through rows needing replacement
1275             for (UntypedRow row: rowsToReplace) {
1276                 for (NamedContentValues ncv: oldValues) {
1277                     ContentValues cv = ncv.values;
1278                     // Find a row that hasn't been used (i.e. doesn't match current rows)
1279                     if (!cv.containsKey(FOUND_DATA_ROW)) {
1280                         // And update it
1281                         RowBuilder builder = new RowBuilder(
1282                                 ContentProviderOperation
1283                                     .newUpdate(addCallerIsSyncAdapterParameter(
1284                                         dataUriFromNamedContentValues(ncv))),
1285                                 ncv);
1286                         row.addValues(builder);
1287                         add(builder.build());
1288                     }
1289                 }
1290             }
1291         }
1292 
1293         public void addOrganization(Entity entity, int type, String company, String title,
1294                 String department, String yomiCompanyName, String officeLocation) {
1295             RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
1296             ContentValues cv = builder.cv;
1297             if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
1298                     cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
1299                     cvCompareString(cv, Organization.DEPARTMENT, department) &&
1300                     cvCompareString(cv, Organization.TITLE, title) &&
1301                     cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
1302                 return;
1303             }
1304             builder.withValue(Organization.TYPE, type);
1305             builder.withValue(Organization.COMPANY, company);
1306             builder.withValue(Organization.TITLE, title);
1307             builder.withValue(Organization.DEPARTMENT, department);
1308             builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
1309             builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
1310             add(builder.build());
1311         }
1312 
1313         public void addNote(Entity entity, String note) {
1314             RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
1315             ContentValues cv = builder.cv;
1316             if (note == null) return;
1317             note = note.replaceAll("\r\n", "\n");
1318             if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
1319                 return;
1320             }
1321 
1322             // Reject notes with nothing in them.  Often, we get something from Outlook when
1323             // nothing was ever entered.  Sigh.
1324             int len = note.length();
1325             int i = 0;
1326             for (; i < len; i++) {
1327                 char c = note.charAt(i);
1328                 if (!Character.isWhitespace(c)) {
1329                     break;
1330                 }
1331             }
1332             if (i == len) return;
1333 
1334             builder.withValue(Note.NOTE, note);
1335             add(builder.build());
1336         }
1337     }
1338 
1339     @Override
1340     protected void wipe() {
1341         LogUtils.w(TAG, "Wiping contacts for account %d", mAccount.mId);
1342         EasSyncContacts.wipeAccountFromContentProvider(mContext,
1343                 mAccount.mEmailAddress);
1344     }
1345 }
1346