Home | History | Annotate | Download | only in eas
      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 {
    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