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