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