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