1package com.android.exchange.eas;
2
3import android.content.ContentProviderOperation;
4import android.content.ContentResolver;
5import android.content.ContentUris;
6import android.content.ContentValues;
7import android.content.Context;
8import android.content.Entity;
9import android.content.EntityIterator;
10import android.database.Cursor;
11import android.net.Uri;
12import android.provider.ContactsContract;
13import android.provider.ContactsContract.CommonDataKinds.Email;
14import android.provider.ContactsContract.CommonDataKinds.Event;
15import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
16import android.provider.ContactsContract.CommonDataKinds.Im;
17import android.provider.ContactsContract.CommonDataKinds.Nickname;
18import android.provider.ContactsContract.CommonDataKinds.Note;
19import android.provider.ContactsContract.CommonDataKinds.Organization;
20import android.provider.ContactsContract.CommonDataKinds.Phone;
21import android.provider.ContactsContract.CommonDataKinds.Photo;
22import android.provider.ContactsContract.CommonDataKinds.Relation;
23import android.provider.ContactsContract.CommonDataKinds.StructuredName;
24import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
25import android.provider.ContactsContract.CommonDataKinds.Website;
26import android.provider.ContactsContract.Groups;
27import android.text.TextUtils;
28import android.util.Base64;
29
30import com.android.emailcommon.TrafficFlags;
31import com.android.emailcommon.provider.Account;
32import com.android.emailcommon.provider.Mailbox;
33import com.android.emailcommon.utility.Utility;
34import com.android.exchange.Eas;
35import com.android.exchange.adapter.AbstractSyncParser;
36import com.android.exchange.adapter.ContactsSyncParser;
37import com.android.exchange.adapter.Serializer;
38import com.android.exchange.adapter.Tags;
39import com.android.mail.utils.LogUtils;
40
41import java.io.IOException;
42import java.io.InputStream;
43import java.text.DateFormat;
44import java.text.ParseException;
45import java.text.SimpleDateFormat;
46import java.util.ArrayList;
47import java.util.Date;
48import java.util.Locale;
49import 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 */
56public 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 {
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
156    public EasSyncContacts(final String emailAddress) {
157        mAccountManagerAccount = new android.accounts.Account(emailAddress,
158                Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
159    }
160
161    @Override
162    public int getTrafficFlag() {
163        return TrafficFlags.DATA_CONTACTS;
164    }
165
166    @Override
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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.
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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