1 package com.android.exchange.eas;
2 
3 import android.content.ContentProviderOperation;
4 import android.content.ContentResolver;
5 import android.content.ContentUris;
6 import android.content.ContentValues;
7 import android.content.Context;
8 import android.content.Entity;
9 import android.content.EntityIterator;
10 import android.database.Cursor;
11 import android.net.Uri;
12 import android.provider.ContactsContract;
13 import android.provider.ContactsContract.CommonDataKinds.Email;
14 import android.provider.ContactsContract.CommonDataKinds.Event;
15 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
16 import android.provider.ContactsContract.CommonDataKinds.Im;
17 import android.provider.ContactsContract.CommonDataKinds.Nickname;
18 import android.provider.ContactsContract.CommonDataKinds.Note;
19 import android.provider.ContactsContract.CommonDataKinds.Organization;
20 import android.provider.ContactsContract.CommonDataKinds.Phone;
21 import android.provider.ContactsContract.CommonDataKinds.Photo;
22 import android.provider.ContactsContract.CommonDataKinds.Relation;
23 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
24 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
25 import android.provider.ContactsContract.CommonDataKinds.Website;
26 import android.provider.ContactsContract.Groups;
27 import android.text.TextUtils;
28 import android.util.Base64;
29 
30 import com.android.emailcommon.TrafficFlags;
31 import com.android.emailcommon.provider.Account;
32 import com.android.emailcommon.provider.Mailbox;
33 import com.android.emailcommon.utility.Utility;
34 import com.android.exchange.Eas;
35 import com.android.exchange.adapter.AbstractSyncParser;
36 import com.android.exchange.adapter.ContactsSyncParser;
37 import com.android.exchange.adapter.Serializer;
38 import com.android.exchange.adapter.Tags;
39 import com.android.mail.utils.LogUtils;
40 
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.text.DateFormat;
44 import java.text.ParseException;
45 import java.text.SimpleDateFormat;
46 import java.util.ArrayList;
47 import java.util.Date;
48 import java.util.Locale;
49 import java.util.TimeZone;
50 
51 /**
52  * Performs an Exchange sync for contacts.
53  * Contact state is in the contacts provider, not in our DB (and therefore not in e.g. mMailbox).
54  * The Mailbox in the Email DB is only useful for serverId and syncInterval.
55  */
56 public class EasSyncContacts extends EasSyncCollectionTypeBase {
57     private static final String TAG = Eas.LOG_TAG;
58 
59     public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
60 
61     private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS =
62             ContactsContract.Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " +
63                     GroupMembership.GROUP_ROW_ID + "=?";
64 
65     private static final String[] GROUP_TITLE_PROJECTION =
66             new String[] {Groups.TITLE};
67     private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
68 
69     /** The maximum number of IMs we can send for one contact. */
70     private static final int MAX_IM_ROWS = 3;
71     /** The tags to use for IMs in an upsync. */
72     private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
73             Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
74 
75     /** The maximum number of email addresses we can send for one contact. */
76     private static final int MAX_EMAIL_ROWS = 3;
77     /** The tags to use for the emails in an upsync. */
78     private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
79             Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
80 
81     /** The maximum number of phone numbers of each type we can send for one contact. */
82     private static final int MAX_PHONE_ROWS = 2;
83     /** The tags to use for work phone numbers. */
84     private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
85             Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
86     /** The tags to use for home phone numbers. */
87     private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
88             Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
89 
90     /** The tags to use for different parts of a home address. */
91     private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
92             Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
93             Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
94             Tags.CONTACTS_HOME_ADDRESS_STATE,
95             Tags.CONTACTS_HOME_ADDRESS_STREET};
96 
97     /** The tags to use for different parts of a work address. */
98     private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
99             Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
100             Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
101             Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
102             Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
103 
104     /** The tags to use for different parts of an "other" address. */
105     private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
106             Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
107             Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
108             Tags.CONTACTS_OTHER_ADDRESS_STATE,
109             Tags.CONTACTS_OTHER_ADDRESS_STREET};
110 
111     private final android.accounts.Account mAccountManagerAccount;
112 
113     private final ArrayList<Long> mDeletedContacts = new ArrayList<Long>();
114     private final ArrayList<Long> mUpdatedContacts = new ArrayList<Long>();
115 
116     // We store the parser so that we can ask it later isGroupsUsed.
117     // TODO: Can we do this more cleanly?
118     private ContactsSyncParser mParser = null;
119 
120     private static final class EasChildren {
EasChildren()121         private EasChildren() {}
122 
123         /** MIME type used when storing this in data table. */
124         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
125         public static final int MAX_CHILDREN = 8;
126         public static final String[] ROWS =
127             new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
128     }
129 
130     // Classes for each type of contact.
131     // These are copied from ContactSyncAdapter, with unused fields and methods removed, but the
132     // parser hasn't been moved over yet. When that happens, the variables and functions may also
133     // need to be copied over.
134 
135     /**
136      * Data and constants for a Personal contact.
137      */
138     private static final class EasPersonal {
139         /** MIME type used when storing this in data table. */
140         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
141         public static final String ANNIVERSARY = "data2";
142         public static final String FILE_AS = "data4";
143     }
144 
145     /**
146      * Data and constants for a Business contact.
147      */
148     private static final class EasBusiness {
149         /** MIME type used when storing this in data table. */
150         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
151         public static final String CUSTOMER_ID = "data6";
152         public static final String GOVERNMENT_ID = "data7";
153         public static final String ACCOUNT_NAME = "data8";
154     }
155 
EasSyncContacts(final String emailAddress)156     public EasSyncContacts(final String emailAddress) {
157         mAccountManagerAccount = new android.accounts.Account(emailAddress,
158                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
159     }
160 
161     @Override
getTrafficFlag()162     public int getTrafficFlag() {
163         return TrafficFlags.DATA_CONTACTS;
164     }
165 
166     @Override
setSyncOptions(final Context context, final Serializer s, final double protocolVersion, final Account account, final Mailbox mailbox, final boolean isInitialSync, final int numWindows)167     public void setSyncOptions(final Context context, final Serializer s,
168             final double protocolVersion, final Account account, final Mailbox mailbox,
169             final boolean isInitialSync, final int numWindows) throws IOException {
170         if (isInitialSync) {
171             setInitialSyncOptions(s);
172             return;
173         }
174 
175         final int windowSize = numWindows * PIM_WINDOW_SIZE_CONTACTS;
176         if (windowSize > MAX_WINDOW_SIZE  + PIM_WINDOW_SIZE_CONTACTS) {
177             throw new IOException("Max window size reached and still no data");
178         }
179         setPimSyncOptions(s, null, protocolVersion,
180                 windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE);
181 
182         setUpsyncCommands(s, context.getContentResolver(), account, mailbox, protocolVersion);
183     }
184 
185     @Override
getParser(final Context context, final Account account, final Mailbox mailbox, final InputStream is)186     public AbstractSyncParser getParser(final Context context, final Account account,
187             final Mailbox mailbox, final InputStream is) throws IOException {
188         mParser = new ContactsSyncParser(context, context.getContentResolver(), is, mailbox,
189                 account, mAccountManagerAccount);
190         return mParser;
191     }
192 
setInitialSyncOptions(final Serializer s)193     private void setInitialSyncOptions(final Serializer s) throws IOException {
194         // These are the tags we support for upload; whenever we add/remove support
195         // (in addData), we need to update this list
196         s.start(Tags.SYNC_SUPPORTED);
197         s.tag(Tags.CONTACTS_FIRST_NAME);
198         s.tag(Tags.CONTACTS_LAST_NAME);
199         s.tag(Tags.CONTACTS_MIDDLE_NAME);
200         s.tag(Tags.CONTACTS_SUFFIX);
201         s.tag(Tags.CONTACTS_COMPANY_NAME);
202         s.tag(Tags.CONTACTS_JOB_TITLE);
203         s.tag(Tags.CONTACTS_EMAIL1_ADDRESS);
204         s.tag(Tags.CONTACTS_EMAIL2_ADDRESS);
205         s.tag(Tags.CONTACTS_EMAIL3_ADDRESS);
206         s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER);
207         s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER);
208         s.tag(Tags.CONTACTS2_MMS);
209         s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER);
210         s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE);
211         s.tag(Tags.CONTACTS_HOME_FAX_NUMBER);
212         s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER);
213         s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER);
214         s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER);
215         s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER);
216         s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER);
217         s.tag(Tags.CONTACTS_PAGER_NUMBER);
218         s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER);
219         s.tag(Tags.CONTACTS2_IM_ADDRESS);
220         s.tag(Tags.CONTACTS2_IM_ADDRESS_2);
221         s.tag(Tags.CONTACTS2_IM_ADDRESS_3);
222         s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY);
223         s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY);
224         s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE);
225         s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE);
226         s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET);
227         s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY);
228         s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY);
229         s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE);
230         s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE);
231         s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET);
232         s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY);
233         s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY);
234         s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE);
235         s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE);
236         s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET);
237         s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME);
238         s.tag(Tags.CONTACTS_YOMI_FIRST_NAME);
239         s.tag(Tags.CONTACTS_YOMI_LAST_NAME);
240         s.tag(Tags.CONTACTS2_NICKNAME);
241         s.tag(Tags.CONTACTS_ASSISTANT_NAME);
242         s.tag(Tags.CONTACTS2_MANAGER_NAME);
243         s.tag(Tags.CONTACTS_SPOUSE);
244         s.tag(Tags.CONTACTS_DEPARTMENT);
245         s.tag(Tags.CONTACTS_TITLE);
246         s.tag(Tags.CONTACTS_OFFICE_LOCATION);
247         s.tag(Tags.CONTACTS2_CUSTOMER_ID);
248         s.tag(Tags.CONTACTS2_GOVERNMENT_ID);
249         s.tag(Tags.CONTACTS2_ACCOUNT_NAME);
250         s.tag(Tags.CONTACTS_ANNIVERSARY);
251         s.tag(Tags.CONTACTS_BIRTHDAY);
252         s.tag(Tags.CONTACTS_WEBPAGE);
253         s.tag(Tags.CONTACTS_PICTURE);
254         s.tag(Tags.CONTACTS_FILE_AS);
255         s.end(); // SYNC_SUPPORTED
256     }
257 
258     /**
259      * Add account info and the "caller is syncadapter" param to a URI.
260      * @param uri The {@link Uri} to add to.
261      * @param emailAddress The email address to add to uri.
262      * @return
263      */
uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress)264     private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) {
265         return uri.buildUpon()
266             .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, emailAddress)
267             .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE,
268                     Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
269             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
270             .build();
271     }
272 
273     /**
274      * Add the "caller is syncadapter" param to a URI.
275      * @param uri The {@link Uri} to add to.
276      * @return
277      */
addCallerIsSyncAdapterParameter(final Uri uri)278     private static Uri addCallerIsSyncAdapterParameter(final Uri uri) {
279         return uri.buildUpon()
280                 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
281                 .build();
282     }
283 
284     /**
285      * Mark contacts in dirty groups as dirty.
286      */
dirtyContactsWithinDirtyGroups(final ContentResolver cr, final Account account)287     private void dirtyContactsWithinDirtyGroups(final ContentResolver cr, final Account account) {
288         final String emailAddress = account.mEmailAddress;
289         final Cursor c = cr.query( uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
290                 GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
291         if (c == null) {
292             return;
293         }
294         try {
295             if (c.getCount() > 0) {
296                 final String[] updateArgs = new String[1];
297                 final ContentValues updateValues = new ContentValues();
298                 while (c.moveToNext()) {
299                     // For each, "touch" all data rows with this group id; this will mark contacts
300                     // in this group as dirty (per ContactsContract).  We will then know to upload
301                     // them to the server with the modified group information
302                     final long id = c.getLong(0);
303                     updateValues.put(GroupMembership.GROUP_ROW_ID, id);
304                     updateArgs[0] = Long.toString(id);
305                     cr.update(ContactsContract.Data.CONTENT_URI, updateValues,
306                             MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
307                 }
308                 // Really delete groups that are marked deleted
309                 cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
310                         Groups.DELETED + "=1", null);
311                 // Clear the dirty flag for all of our groups
312                 updateValues.clear();
313                 updateValues.put(Groups.DIRTY, 0);
314                 cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
315                         updateValues, null, null);
316             }
317         } finally {
318             c.close();
319         }
320     }
321 
322     /**
323      * Helper function to safely extract a string from a content value.
324      * @param cv The {@link ContentValues} that contains the values
325      * @param column The column name in cv for the data
326      * @return The data in the column or null if it doesn't exist or is empty.
327      * @throws IOException
328      */
tryGetStringData(final ContentValues cv, final String column)329     public static String tryGetStringData(final ContentValues cv, final String column)
330             throws IOException {
331         if ((cv == null) || (column == null)) {
332             return null;
333         }
334         if (cv.containsKey(column)) {
335             final String value = cv.getAsString(column);
336             if (!TextUtils.isEmpty(value)) {
337                 return value;
338             }
339         }
340         return null;
341     }
342 
343     /**
344      * Helper to add a string to the upsync.
345      * @param s The {@link Serializer} for this sync request
346      * @param cv The {@link ContentValues} with the data for this string.
347      * @param column The column name in cv to find the string.
348      * @param tag The tag to use when adding to s.
349      * @return Whether or not the field was actually set.
350      * @throws IOException
351      */
sendStringData(final Serializer s, final ContentValues cv, final String column, final int tag)352     private static boolean sendStringData(final Serializer s, final ContentValues cv,
353             final String column, final int tag) throws IOException {
354         final String dataValue = tryGetStringData(cv, column);
355         if (dataValue != null) {
356             s.data(tag, dataValue);
357             return true;
358         }
359         return false;
360     }
361 
362 
363     // This is to catch when the contacts provider has a date in this particular wrong format.
364     private static final SimpleDateFormat SHORT_DATE_FORMAT;
365     // Array of formats we check when parsing dates from the contacts provider.
366     private static final DateFormat[] DATE_FORMATS;
367     static {
368         SHORT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
369         SHORT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
370         //TODO: We only handle two formatting types. The default contacts app will work with this
371         // but any other contacts apps might not. We can try harder to handle those guys too.
372         DATE_FORMATS = new DateFormat[] { Eas.DATE_FORMAT, SHORT_DATE_FORMAT };
373     }
374 
375     /**
376      * Helper to add a date to the upsync. It reads the date as a string from the
377      * {@link ContentValues} that we got from the provider, tries to parse it using various formats,
378      * and formats it correctly to send to the server. If it can't parse it, it will omit the date
379      * in the upsync; since Birthdays (the only date currently supported by this class) can be
380      * ghosted, this means that any date changes on the client will NOT be reflected on the server.
381      * @param s The {@link Serializer} for this sync request
382      * @param cv The {@link ContentValues} with the data for this string.
383      * @param column The column name in cv to find the string.
384      * @param tag The tag to use when adding to s.
385      * @throws IOException
386      */
sendDateData(final Serializer s, final ContentValues cv, final String column, final int tag)387     private static void sendDateData(final Serializer s, final ContentValues cv,
388             final String column, final int tag) throws IOException {
389         if (cv.containsKey(column)) {
390             final String value = cv.getAsString(column);
391             if (!TextUtils.isEmpty(value)) {
392                 Date date;
393                 // Check all the formats we know about to see if one of them works.
394                 for (final DateFormat format : DATE_FORMATS) {
395                     try {
396                         date = format.parse(value);
397                         if (date != null) {
398                             // We got a legit date for this format, so send it up.
399                             s.data(tag, Eas.DATE_FORMAT.format(date));
400                             return;
401                         }
402                     } catch (final ParseException e) {
403                         // The date didn't match this particular format; keep looping.
404                     }
405                 }
406             }
407         }
408     }
409 
410 
411     /**
412      * Add a nickname to the upsync.
413      * @param s The {@link Serializer} for this sync request.
414      * @param cv The {@link ContentValues} with the data for this nickname.
415      * @throws IOException
416      */
sendNickname(final Serializer s, final ContentValues cv)417     private static void sendNickname(final Serializer s, final ContentValues cv)
418             throws IOException {
419         sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
420     }
421 
422     /**
423      * Add children data to the upsync.
424      * @param s The {@link Serializer} for this sync request.
425      * @param cv The {@link ContentValues} with the data for a set of children.
426      * @throws IOException
427      */
sendChildren(final Serializer s, final ContentValues cv)428     private static void sendChildren(final Serializer s, final ContentValues cv)
429             throws IOException {
430         boolean first = true;
431         for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
432             final String row = EasChildren.ROWS[i];
433             if (cv.containsKey(row)) {
434                 if (first) {
435                     s.start(Tags.CONTACTS_CHILDREN);
436                     first = false;
437                 }
438                 s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
439             }
440         }
441         if (!first) {
442             s.end();
443         }
444     }
445 
446     /**
447      * Add business contact info to the upsync.
448      * @param s The {@link Serializer} for this sync request.
449      * @param cv The {@link ContentValues} with the data for this business contact.
450      * @throws IOException
451      */
sendBusiness(final Serializer s, final ContentValues cv)452     private static void sendBusiness(final Serializer s, final ContentValues cv)
453             throws IOException {
454         sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
455         sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
456         sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID);
457     }
458 
459     /**
460      * Add a webpage info to the upsync.
461      * @param s The {@link Serializer} for this sync request.
462      * @param cv The {@link ContentValues} with the data for this webpage.
463      * @throws IOException
464      */
sendWebpage(final Serializer s, final ContentValues cv)465     private static void sendWebpage(final Serializer s, final ContentValues cv) throws IOException {
466         sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
467     }
468 
469     /**
470      * Add personal contact info to the upsync.
471      * @param s The {@link Serializer} for this sync request.
472      * @param cv The {@link ContentValues} with the data for this personal contact.
473      * @throws IOException
474      */
sendPersonal(final Serializer s, final ContentValues cv)475     private static void sendPersonal(final Serializer s, final ContentValues cv)
476             throws IOException {
477         sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
478     }
479 
480     /**
481      * Add contact file_as info to the upsync.
482      * @param s The {@link Serializer} for this sync request.
483      * @param cv The {@link ContentValues} with the data for this personal contact.
484      * @throws IOException
485      */
trySendFileAs(final Serializer s, final ContentValues cv)486     private static boolean trySendFileAs(final Serializer s, final ContentValues cv)
487             throws IOException {
488         return sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
489     }
490 
491     /**
492      * Add a phone number to the upsync.
493      * @param s The {@link Serializer} for this sync request.
494      * @param cv The {@link ContentValues} with the data for this phone number.
495      * @param workCount The number of work phone numbers already added.
496      * @param homeCount The number of home phone numbers already added.
497      * @throws IOException
498      */
sendPhone(final Serializer s, final ContentValues cv, final int workCount, final int homeCount)499     private static void sendPhone(final Serializer s, final ContentValues cv, final int workCount,
500             final int homeCount) throws IOException {
501         final String value = cv.getAsString(Phone.NUMBER);
502         if (value == null || !cv.containsKey(Phone.TYPE)) {
503             return;
504         }
505         switch (cv.getAsInteger(Phone.TYPE)) {
506             case Phone.TYPE_WORK:
507                 if (workCount < MAX_PHONE_ROWS) {
508                     s.data(WORK_PHONE_TAGS[workCount], value);
509                 }
510                 break;
511             case Phone.TYPE_MMS:
512                 s.data(Tags.CONTACTS2_MMS, value);
513                 break;
514             case Phone.TYPE_ASSISTANT:
515                 s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
516                 break;
517             case Phone.TYPE_FAX_WORK:
518                 s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
519                 break;
520             case Phone.TYPE_COMPANY_MAIN:
521                 s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
522                 break;
523             case Phone.TYPE_HOME:
524                 if (homeCount < MAX_PHONE_ROWS) {
525                     s.data(HOME_PHONE_TAGS[homeCount], value);
526                 }
527                 break;
528             case Phone.TYPE_MOBILE:
529                 s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
530                 break;
531             case Phone.TYPE_CAR:
532                 s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
533                 break;
534             case Phone.TYPE_PAGER:
535                 s.data(Tags.CONTACTS_PAGER_NUMBER, value);
536                 break;
537             case Phone.TYPE_RADIO:
538                 s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
539                 break;
540             case Phone.TYPE_FAX_HOME:
541                 s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
542                 break;
543             default:
544                 break;
545         }
546     }
547 
548     /**
549      * Add a relation to the upsync.
550      * @param s The {@link Serializer} for this sync request.
551      * @param cv The {@link ContentValues} with the data for this relation.
552      * @throws IOException
553      */
sendRelation(final Serializer s, final ContentValues cv)554     private static void sendRelation(final Serializer s, final ContentValues cv)
555             throws IOException {
556         final String value = cv.getAsString(Relation.DATA);
557         if (value == null || !cv.containsKey(Relation.TYPE)) {
558             return;
559         }
560         switch (cv.getAsInteger(Relation.TYPE)) {
561             case Relation.TYPE_ASSISTANT:
562                 s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
563                 break;
564             case Relation.TYPE_MANAGER:
565                 s.data(Tags.CONTACTS2_MANAGER_NAME, value);
566                 break;
567             case Relation.TYPE_SPOUSE:
568                 s.data(Tags.CONTACTS_SPOUSE, value);
569                 break;
570             default:
571                 break;
572         }
573     }
574 
575     /**
576      * Add a name to the upsync.
577      * @param s The {@link Serializer} for this sync request.
578      * @param cv The {@link ContentValues} with the data for this name.
579      * @throws IOException
580      */
581     // TODO: This used to return a displayName, but it was always null. Figure out what it really
582     // wanted to return.
sendStructuredName(final Serializer s, final ContentValues cv)583     private static void sendStructuredName(final Serializer s, final ContentValues cv)
584             throws IOException {
585         sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
586         sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
587         sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
588         sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
589         sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
590         sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
591         sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
592     }
593 
594     /**
595      * Add an address of a particular type to the upsync.
596      * @param s The {@link Serializer} for this sync request.
597      * @param cv The {@link ContentValues} with the data for this address.
598      * @param fieldNames The field names for this address type.
599      * @throws IOException
600      */
sendOnePostal(final Serializer s, final ContentValues cv, final int[] fieldNames)601     private static void sendOnePostal(final Serializer s, final ContentValues cv,
602             final int[] fieldNames) throws IOException{
603         sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
604         sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
605         sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
606         sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
607         sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
608     }
609 
610     /**
611      * Add an address to the upsync.
612      * @param s The {@link Serializer} for this sync request.
613      * @param cv The {@link ContentValues} with the data for this address.
614      * @throws IOException
615      */
sendStructuredPostal(final Serializer s, final ContentValues cv)616     private static void sendStructuredPostal(final Serializer s, final ContentValues cv)
617         throws IOException {
618         if (!cv.containsKey(StructuredPostal.TYPE)) {
619             return;
620         }
621         switch (cv.getAsInteger(StructuredPostal.TYPE)) {
622             case StructuredPostal.TYPE_HOME:
623                 sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
624                 break;
625             case StructuredPostal.TYPE_WORK:
626                 sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
627                 break;
628             case StructuredPostal.TYPE_OTHER:
629                 sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
630                 break;
631             default:
632                 break;
633         }
634     }
635 
636     /**
637      * Add an organization to the upsync.
638      * @param s The {@link Serializer} for this sync request.
639      * @param cv The {@link ContentValues} with the data for this organization.
640      * @throws IOException
641      */
sendOrganization(final Serializer s, final ContentValues cv)642     private static void sendOrganization(final Serializer s, final ContentValues cv)
643             throws IOException {
644         sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
645         sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
646         sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
647         sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION);
648     }
649 
650     /**
651      * Add an IM to the upsync.
652      * @param s The {@link Serializer} for this sync request.
653      * @param cv The {@link ContentValues} with the data for this IM.
654      * @throws IOException
655      */
sendIm(final Serializer s, final ContentValues cv, final int count)656      private static void sendIm(final Serializer s, final ContentValues cv, final int count)
657              throws IOException {
658         final String value = cv.getAsString(Im.DATA);
659         if (value == null) return;
660         if (count < MAX_IM_ROWS) {
661             s.data(IM_TAGS[count], value);
662         }
663     }
664 
665     /**
666      * Add a birthday to the upsync.
667      * @param s The {@link Serializer} for this sync request.
668      * @param cv The {@link ContentValues} with the data for this birthday.
669      * @throws IOException
670      */
sendBirthday(final Serializer s, final ContentValues cv)671     private static void sendBirthday(final Serializer s, final ContentValues cv)
672             throws IOException {
673         sendDateData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
674     }
675 
676     /**
677      * Add a note to the upsync.
678      * @param s The {@link Serializer} for this sync request.
679      * @param cv The {@link ContentValues} with the data for this note.
680      * @param protocolVersion
681      * @throws IOException
682      */
sendNote(final Serializer s, final ContentValues cv, final double protocolVersion)683     private void sendNote(final Serializer s, final ContentValues cv, final double protocolVersion)
684             throws IOException {
685         // Even when there is no local note, we must explicitly upsync an empty note,
686         // which is the only way to force the server to delete any pre-existing note.
687         String note = "";
688         if (cv.containsKey(Note.NOTE)) {
689             // EAS won't accept note data with raw newline characters
690             note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
691         }
692         // Format of upsync data depends on protocol version
693         if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
694             s.start(Tags.BASE_BODY);
695             s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
696             s.end();
697         } else {
698             s.data(Tags.CONTACTS_BODY, note);
699         }
700     }
701 
702     /**
703      * Add a photo to the upsync.
704      * @param s The {@link Serializer} for this sync request.
705      * @param cv The {@link ContentValues} with the data for this photo.
706      * @throws IOException
707      */
sendPhoto(final Serializer s, final ContentValues cv)708     private static void sendPhoto(final Serializer s, final ContentValues cv) throws IOException {
709         if (cv.containsKey(Photo.PHOTO)) {
710             final byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
711             final String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
712             s.data(Tags.CONTACTS_PICTURE, pic);
713         } else {
714             // Send an empty tag, which signals the server to delete any pre-existing photo
715             s.tag(Tags.CONTACTS_PICTURE);
716         }
717     }
718 
719     /**
720      * Add an email address to the upsync.
721      * @param s The {@link Serializer} for this sync request.
722      * @param cv The {@link ContentValues} with the data for this email address.
723      * @param count The number of email addresses that have already been added.
724      * @param displayName The display name for this contact.
725      * @param protocolVersion
726      * @throws IOException
727      */
sendEmail(final Serializer s, final ContentValues cv, final int count, final String displayName, final double protocolVersion)728     private void sendEmail(final Serializer s, final ContentValues cv, final int count,
729             final String displayName, final double protocolVersion) throws IOException {
730         // Get both parts of the email address (a newly created one in the UI won't have a name)
731         final String addr = cv.getAsString(Email.DATA);
732         String name = cv.getAsString(Email.DISPLAY_NAME);
733         if (name == null) {
734             if (displayName != null) {
735                 name = displayName;
736             } else {
737                 name = addr;
738             }
739         }
740         // Compose address from name and addr
741         if (addr != null) {
742             final String value;
743             // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on
744             // an RFC822 address)
745             if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
746                 value = addr;
747             } else {
748                 value = '\"' + name + "\" <" + addr + '>';
749             }
750             if (count < MAX_EMAIL_ROWS) {
751                 s.data(EMAIL_TAGS[count], value);
752             }
753         }
754     }
755 
756     /**
757      * Generate a default fileAs string for this contact using name and email data.
758      * Note that the user can change this in Outlook/OWA if it is not correct for them but
759      * we need to send something or else Exchange will not display a name with the contact.
760      * @param nameValues Name information to use in generating the fileAs string
761      * @param emailValues Email information to use to generate the fileAs string
762      * @return A valid fileAs string or null
763      */
generateFileAs(final ContentValues nameValues, final ArrayList<ContentValues> emailValues)764     public static String generateFileAs(final ContentValues nameValues,
765             final ArrayList<ContentValues> emailValues) throws IOException {
766         // TODO: Is there a better way of generating a default file_as that will make people
767         // happy everywhere in the world? Should we read the sort settings of the People app?
768         final String firstName = tryGetStringData(nameValues, StructuredName.GIVEN_NAME);
769         final String lastName = tryGetStringData(nameValues, StructuredName.FAMILY_NAME);;
770         final String middleName = tryGetStringData(nameValues, StructuredName.MIDDLE_NAME);;
771         final String nameSuffix = tryGetStringData(nameValues, StructuredName.SUFFIX);
772 
773         if (firstName == null && lastName == null) {
774             if (emailValues == null) {
775                 // Bad name, bad email list...not much we can do about it.
776                 return null;
777             }
778             // The name fields didn't yield anything valuable, let's generate a file as
779             // via the email addresses that were passed in.
780             for (final ContentValues cv : emailValues) {
781                 final String emailAddr = tryGetStringData(cv, Email.DATA);
782                 if (emailAddr != null) {
783                     return emailAddr;
784                 }
785             }
786             return null;
787         }
788         // Let's try to construct this with the name only. The format is this:
789         // LastName nameSuffix, FirstName MiddleName
790         // nameSuffix is only applied if lastName exists.
791         final StringBuilder builder = new StringBuilder();
792         if (lastName != null) {
793             builder.append(lastName);
794             if (nameSuffix != null) {
795                 builder.append(" " + nameSuffix);
796             }
797             builder.append(", ");
798         }
799         if (firstName != null) {
800             builder.append(firstName + " ");
801         }
802         if (middleName != null) {
803             builder.append(middleName);
804         }
805         // We might leave a trailing space, so let's trim the string here.
806         return builder.toString().trim();
807     }
808 
809 
setUpsyncCommands(final Serializer s, final ContentResolver cr, final Account account, final Mailbox mailbox, final double protocolVersion)810     private void setUpsyncCommands(final Serializer s, final ContentResolver cr,
811             final Account account, final Mailbox mailbox, final double protocolVersion)
812             throws IOException {
813         // Find any groups of ours that are dirty and dirty those groups' members
814         dirtyContactsWithinDirtyGroups(cr, account);
815 
816         // First, let's find Contacts that have changed.
817         final Uri uri = uriWithAccountAndIsSyncAdapter(
818                 ContactsContract.RawContactsEntity.CONTENT_URI, account.mEmailAddress);
819 
820         // Get them all atomically
821         final Cursor cursor = cr.query(uri, null, ContactsContract.RawContacts.DIRTY + "=1",
822                 null, null);
823         if (cursor == null) {
824             return;
825         }
826         final EntityIterator ei = ContactsContract.RawContacts.newEntityIterator(cursor);
827         final ContentValues cidValues = new ContentValues();
828         boolean hasSetFileAs = false;
829         try {
830             boolean first = true;
831             final Uri rawContactUri = addCallerIsSyncAdapterParameter(
832                     ContactsContract.RawContacts.CONTENT_URI);
833             while (ei.hasNext()) {
834                 final Entity entity = ei.next();
835                 // For each of these entities, create the change commands
836                 final ContentValues entityValues = entity.getEntityValues();
837                 String serverId = entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID);
838                 final ArrayList<Integer> groupIds = new ArrayList<Integer>();
839                 if (first) {
840                     s.start(Tags.SYNC_COMMANDS);
841                     LogUtils.d(TAG, "Sending Contacts changes to the server");
842                     first = false;
843                 }
844                 if (serverId == null) {
845                     // This is a new contact; create a clientId
846                     final String clientId =
847                             "new_" + mailbox.mId + '_' + System.currentTimeMillis();
848                     // We need to server id to look up the fileAs string.
849                     serverId = clientId;
850                     LogUtils.d(TAG, "Creating new contact with clientId: %s", clientId);
851                     s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
852                     // And save it in the raw contact
853                     cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
854                     cr.update(ContentUris.withAppendedId(rawContactUri,
855                             entityValues.getAsLong(ContactsContract.RawContacts._ID)),
856                             cidValues, null, null);
857                 } else {
858                     if (entityValues.getAsInteger(ContactsContract.RawContacts.DELETED) == 1) {
859                         LogUtils.d(TAG, "Deleting contact with serverId: %s", serverId);
860                         s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
861                         mDeletedContacts.add(
862                                 entityValues.getAsLong(ContactsContract.RawContacts._ID));
863                         continue;
864                     }
865                     LogUtils.d(TAG, "Upsync change to contact with serverId: %s", serverId);
866                     s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
867                     // We don't need to set the file has because it is not a new contact
868                     // i.e. it should have the file_as if it needs one.
869                     hasSetFileAs = true;
870                 }
871                 s.start(Tags.SYNC_APPLICATION_DATA);
872                 // Write out the data here
873                 int imCount = 0;
874                 int emailCount = 0;
875                 int homePhoneCount = 0;
876                 int workPhoneCount = 0;
877                 // TODO: How is this name supposed to be formed?
878                 String displayName = null;
879                 final ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
880                 ContentValues nameValues = null;
881                 for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
882                     final ContentValues cv = ncv.values;
883                     final String mimeType = cv.getAsString(ContactsContract.Data.MIMETYPE);
884                     if (TextUtils.isEmpty(mimeType)) {
885                         LogUtils.i(TAG, "Contacts upsync, unknown data: no mimetype set");
886                         continue;
887                     }
888 
889                     if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
890                         emailValues.add(cv);
891                     } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
892                         sendNickname(s, cv);
893                     } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
894                         sendChildren(s, cv);
895                     } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
896                         sendBusiness(s, cv);
897                     } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
898                         sendWebpage(s, cv);
899                     } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
900                         sendPersonal(s, cv);
901                         hasSetFileAs = trySendFileAs(s, cv);
902                     } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
903                         sendPhone(s, cv, workPhoneCount, homePhoneCount);
904                         if (cv.containsKey(Phone.TYPE)) {
905                             final int type = cv.getAsInteger(Phone.TYPE);
906                             if (type == Phone.TYPE_HOME) {
907                                 homePhoneCount++;
908                             } else if (type == Phone.TYPE_WORK) {
909                                 workPhoneCount++;
910                             }
911                         }
912                     } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
913                         sendRelation(s, cv);
914                     } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
915                         sendStructuredName(s, cv);
916                         // Stash names here
917                         nameValues = cv;
918                     } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
919                         sendStructuredPostal(s, cv);
920                     } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
921                         sendOrganization(s, cv);
922                     } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
923                         sendIm(s, cv, imCount++);
924                     } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
925                         if (cv.containsKey(Event.TYPE)) {
926                             final Integer eventType = cv.getAsInteger(Event.TYPE);
927                             if (eventType != null &&
928                                     eventType.equals(Event.TYPE_BIRTHDAY)) {
929                                 sendBirthday(s, cv);
930                             }
931                         }
932                     } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
933                         // We must gather these, and send them together (below)
934                         groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
935                     } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
936                         sendNote(s, cv, protocolVersion);
937                     } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
938                         sendPhoto(s, cv);
939                     } else {
940                         LogUtils.i(TAG, "Contacts upsync, unknown data: %s", mimeType);
941                     }
942                 }
943                 // We do the email rows last, because we need to make sure we've found the
944                 // displayName (if one exists); this would be in a StructuredName rnow
945                 for (final ContentValues cv: emailValues) {
946                     sendEmail(s, cv, emailCount++, displayName, protocolVersion);
947                 }
948                 // For Exchange, we need to make sure that we provide a fileAs string because
949                 // it is used as the display name for the contact in some views.
950                 if (!hasSetFileAs) {
951                     String fileAs = null;
952                     // Let's go grab the display_name_alt info for this contact and use
953                     // that as the default fileAs.
954                     final Cursor c = cr.query(ContactsContract.RawContacts.CONTENT_URI,
955                             new String[]{ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE},
956                             ContactsContract.RawContacts.SYNC1 + "=?",
957                             new String[]{String.valueOf(serverId)}, null);
958                     try {
959                         while (c.moveToNext()) {
960                             final String contentValue = c.getString(0);
961                             if ((contentValue != null) && (!TextUtils.isEmpty(contentValue))) {
962                                 fileAs = contentValue;
963                                 break;
964                             }
965                         }
966                     } finally {
967                         c.close();
968                     }
969                     if (fileAs == null) {
970                         // Just in case that property did not exist, we can generate our own
971                         // rudimentary string that uses a combination of structured name fields or
972                         // email addresses depending on what is available.
973                         fileAs = generateFileAs(nameValues, emailValues);
974                     }
975                     s.data(Tags.CONTACTS_FILE_AS, fileAs);
976                 }
977                 // Now, we'll send up groups, if any
978                 if (!groupIds.isEmpty()) {
979                     boolean groupFirst = true;
980                     for (final int id: groupIds) {
981                         // Since we get id's from the provider, we need to find their names
982                         final Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI,
983                                 id), GROUP_TITLE_PROJECTION, null, null, null);
984                         try {
985                             // Presumably, this should always succeed, but ...
986                             if (c.moveToFirst()) {
987                                 if (groupFirst) {
988                                     s.start(Tags.CONTACTS_CATEGORIES);
989                                     groupFirst = false;
990                                 }
991                                 s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
992                             }
993                         } finally {
994                             c.close();
995                         }
996                     }
997                     if (!groupFirst) {
998                         s.end();
999                     }
1000                 }
1001                 s.end().end(); // ApplicationData & Change
1002                 mUpdatedContacts.add(entityValues.getAsLong(ContactsContract.RawContacts._ID));
1003             }
1004             if (!first) {
1005                 s.end(); // Commands
1006             }
1007         } finally {
1008             ei.close();
1009         }
1010 
1011     }
1012 
1013     @Override
cleanup(final Context context, final Account account)1014     public void cleanup(final Context context, final Account account) {
1015         final ContentResolver cr = context.getContentResolver();
1016 
1017         // Mark the changed contacts dirty = 0
1018         // Permanently delete the user deletions
1019         ContactsSyncParser.ContactOperations ops = new ContactsSyncParser.ContactOperations();
1020         for (final Long id: mUpdatedContacts) {
1021             ops.add(ContentProviderOperation
1022                     .newUpdate(ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
1023                             id).buildUpon()
1024                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
1025                             .build())
1026                     .withValue(ContactsContract.RawContacts.DIRTY, 0).build());
1027         }
1028         for (final Long id: mDeletedContacts) {
1029             ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId(
1030                     ContactsContract.RawContacts.CONTENT_URI, id).buildUpon()
1031                     .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
1032                     .build());
1033         }
1034         ops.execute(context);
1035         if (mParser != null && mParser.isGroupsUsed()) {
1036             // Make sure the title column is set for all of our groups
1037             // And that all of our groups are visible
1038             // TODO Perhaps the visible part should only happen when the group is created, but
1039             // this is fine for now.
1040             final Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI,
1041                     account.mEmailAddress);
1042             final Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
1043                     Groups.TITLE + " IS NULL", null, null);
1044             final ContentValues values = new ContentValues();
1045             values.put(Groups.GROUP_VISIBLE, 1);
1046             try {
1047                 while (c.moveToNext()) {
1048                     final String sourceId = c.getString(0);
1049                     values.put(Groups.TITLE, sourceId);
1050                     cr.update(uriWithAccountAndIsSyncAdapter(groupsUri,
1051                             account.mEmailAddress), values, Groups.SOURCE_ID + "=?",
1052                             new String[] {sourceId});
1053                 }
1054             } finally {
1055                 c.close();
1056             }
1057         }
1058     }
1059 
1060     /**
1061      * Delete an account from the Contacts provider.
1062      * @param context Our {@link Context}
1063      * @param emailAddress The email address of the account we wish to delete
1064      */
wipeAccountFromContentProvider(final Context context, final String emailAddress)1065     public static void wipeAccountFromContentProvider(final Context context,
1066             final String emailAddress) {
1067         try {
1068             context.getContentResolver().delete(uriWithAccountAndIsSyncAdapter(
1069                             ContactsContract.RawContacts.CONTENT_URI, emailAddress), null, null);
1070         } catch (IllegalArgumentException e) {
1071             LogUtils.e(TAG, "ContactsProvider disabled; unable to wipe account.");
1072         }
1073     }
1074 }
1075