blob: 3a6ffc05388059229890363997329c352de45cb2 [file] [log] [blame]
package com.android.exchange.eas;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Entity;
import android.content.EntityIterator;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Groups;
import android.text.TextUtils;
import android.util.Base64;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Mailbox;
import com.android.exchange.Eas;
import com.android.exchange.adapter.AbstractSyncParser;
import com.android.exchange.adapter.ContactsSyncParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* Performs an Exchange sync for contacts.
* Contact state is in the contacts provider, not in our DB (and therefore not in e.g. mMailbox).
* The Mailbox in the Email DB is only useful for serverId and syncInterval.
*/
public class EasSyncContacts extends EasSyncCollectionTypeBase {
private static final String TAG = Eas.LOG_TAG;
public static final int PIM_WINDOW_SIZE_CONTACTS = 10;
private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS =
ContactsContract.Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "' AND " +
GroupMembership.GROUP_ROW_ID + "=?";
private static final String[] GROUP_TITLE_PROJECTION =
new String[] {Groups.TITLE};
private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
/** The maximum number of IMs we can send for one contact. */
private static final int MAX_IM_ROWS = 3;
/** The tags to use for IMs in an upsync. */
private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
/** The maximum number of email addresses we can send for one contact. */
private static final int MAX_EMAIL_ROWS = 3;
/** The tags to use for the emails in an upsync. */
private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
/** The maximum number of phone numbers of each type we can send for one contact. */
private static final int MAX_PHONE_ROWS = 2;
/** The tags to use for work phone numbers. */
private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
/** The tags to use for home phone numbers. */
private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
/** The tags to use for different parts of a home address. */
private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
Tags.CONTACTS_HOME_ADDRESS_STATE,
Tags.CONTACTS_HOME_ADDRESS_STREET};
/** The tags to use for different parts of a work address. */
private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
/** The tags to use for different parts of an "other" address. */
private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
Tags.CONTACTS_OTHER_ADDRESS_STATE,
Tags.CONTACTS_OTHER_ADDRESS_STREET};
private final android.accounts.Account mAccountManagerAccount;
private final ArrayList<Long> mDeletedContacts = new ArrayList<Long>();
private final ArrayList<Long> mUpdatedContacts = new ArrayList<Long>();
// We store the parser so that we can ask it later isGroupsUsed.
// TODO: Can we do this more cleanly?
private ContactsSyncParser mParser = null;
private static final class EasChildren {
private EasChildren() {}
/** MIME type used when storing this in data table. */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
public static final int MAX_CHILDREN = 8;
public static final String[] ROWS =
new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
}
// Classes for each type of contact.
// These are copied from ContactSyncAdapter, with unused fields and methods removed, but the
// parser hasn't been moved over yet. When that happens, the variables and functions may also
// need to be copied over.
/**
* Data and constants for a Personal contact.
*/
private static final class EasPersonal {
/** MIME type used when storing this in data table. */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
public static final String ANNIVERSARY = "data2";
public static final String FILE_AS = "data4";
}
/**
* Data and constants for a Business contact.
*/
private static final class EasBusiness {
/** MIME type used when storing this in data table. */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
public static final String CUSTOMER_ID = "data6";
public static final String GOVERNMENT_ID = "data7";
public static final String ACCOUNT_NAME = "data8";
}
public EasSyncContacts(final String emailAddress) {
mAccountManagerAccount = new android.accounts.Account(emailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
}
@Override
public int getTrafficFlag() {
return TrafficFlags.DATA_CONTACTS;
}
@Override
public void setSyncOptions(final Context context, final Serializer s,
final double protocolVersion, final Account account, final Mailbox mailbox,
final boolean isInitialSync, final int numWindows) throws IOException {
if (isInitialSync) {
setInitialSyncOptions(s);
return;
}
final int windowSize = numWindows * PIM_WINDOW_SIZE_CONTACTS;
if (windowSize > MAX_WINDOW_SIZE + PIM_WINDOW_SIZE_CONTACTS) {
throw new IOException("Max window size reached and still no data");
}
setPimSyncOptions(s, null, protocolVersion,
windowSize < MAX_WINDOW_SIZE ? windowSize : MAX_WINDOW_SIZE);
setUpsyncCommands(s, context.getContentResolver(), account, mailbox, protocolVersion);
}
@Override
public AbstractSyncParser getParser(final Context context, final Account account,
final Mailbox mailbox, final InputStream is) throws IOException {
mParser = new ContactsSyncParser(context, context.getContentResolver(), is, mailbox,
account, mAccountManagerAccount);
return mParser;
}
private void setInitialSyncOptions(final Serializer s) throws IOException {
// These are the tags we support for upload; whenever we add/remove support
// (in addData), we need to update this list
s.start(Tags.SYNC_SUPPORTED);
s.tag(Tags.CONTACTS_FIRST_NAME);
s.tag(Tags.CONTACTS_LAST_NAME);
s.tag(Tags.CONTACTS_MIDDLE_NAME);
s.tag(Tags.CONTACTS_SUFFIX);
s.tag(Tags.CONTACTS_COMPANY_NAME);
s.tag(Tags.CONTACTS_JOB_TITLE);
s.tag(Tags.CONTACTS_EMAIL1_ADDRESS);
s.tag(Tags.CONTACTS_EMAIL2_ADDRESS);
s.tag(Tags.CONTACTS_EMAIL3_ADDRESS);
s.tag(Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS2_MMS);
s.tag(Tags.CONTACTS_BUSINESS_FAX_NUMBER);
s.tag(Tags.CONTACTS2_COMPANY_MAIN_PHONE);
s.tag(Tags.CONTACTS_HOME_FAX_NUMBER);
s.tag(Tags.CONTACTS_HOME_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_HOME2_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_CAR_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS_PAGER_NUMBER);
s.tag(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER);
s.tag(Tags.CONTACTS2_IM_ADDRESS);
s.tag(Tags.CONTACTS2_IM_ADDRESS_2);
s.tag(Tags.CONTACTS2_IM_ADDRESS_3);
s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_CITY);
s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY);
s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE);
s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STATE);
s.tag(Tags.CONTACTS_BUSINESS_ADDRESS_STREET);
s.tag(Tags.CONTACTS_HOME_ADDRESS_CITY);
s.tag(Tags.CONTACTS_HOME_ADDRESS_COUNTRY);
s.tag(Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE);
s.tag(Tags.CONTACTS_HOME_ADDRESS_STATE);
s.tag(Tags.CONTACTS_HOME_ADDRESS_STREET);
s.tag(Tags.CONTACTS_OTHER_ADDRESS_CITY);
s.tag(Tags.CONTACTS_OTHER_ADDRESS_COUNTRY);
s.tag(Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE);
s.tag(Tags.CONTACTS_OTHER_ADDRESS_STATE);
s.tag(Tags.CONTACTS_OTHER_ADDRESS_STREET);
s.tag(Tags.CONTACTS_YOMI_COMPANY_NAME);
s.tag(Tags.CONTACTS_YOMI_FIRST_NAME);
s.tag(Tags.CONTACTS_YOMI_LAST_NAME);
s.tag(Tags.CONTACTS2_NICKNAME);
s.tag(Tags.CONTACTS_ASSISTANT_NAME);
s.tag(Tags.CONTACTS2_MANAGER_NAME);
s.tag(Tags.CONTACTS_SPOUSE);
s.tag(Tags.CONTACTS_DEPARTMENT);
s.tag(Tags.CONTACTS_TITLE);
s.tag(Tags.CONTACTS_OFFICE_LOCATION);
s.tag(Tags.CONTACTS2_CUSTOMER_ID);
s.tag(Tags.CONTACTS2_GOVERNMENT_ID);
s.tag(Tags.CONTACTS2_ACCOUNT_NAME);
s.tag(Tags.CONTACTS_ANNIVERSARY);
s.tag(Tags.CONTACTS_BIRTHDAY);
s.tag(Tags.CONTACTS_WEBPAGE);
s.tag(Tags.CONTACTS_PICTURE);
s.end(); // SYNC_SUPPORTED
}
/**
* Add account info and the "caller is syncadapter" param to a URI.
* @param uri The {@link Uri} to add to.
* @param emailAddress The email address to add to uri.
* @return
*/
private static Uri uriWithAccountAndIsSyncAdapter(final Uri uri, final String emailAddress) {
return uri.buildUpon()
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, emailAddress)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
/**
* Add the "caller is syncadapter" param to a URI.
* @param uri The {@link Uri} to add to.
* @return
*/
private static Uri addCallerIsSyncAdapterParameter(final Uri uri) {
return uri.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
/**
* Mark contacts in dirty groups as dirty.
*/
private void dirtyContactsWithinDirtyGroups(final ContentResolver cr, final Account account) {
final String emailAddress = account.mEmailAddress;
final Cursor c = cr.query( uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
if (c == null) {
return;
}
try {
if (c.getCount() > 0) {
final String[] updateArgs = new String[1];
final ContentValues updateValues = new ContentValues();
while (c.moveToNext()) {
// For each, "touch" all data rows with this group id; this will mark contacts
// in this group as dirty (per ContactsContract). We will then know to upload
// them to the server with the modified group information
final long id = c.getLong(0);
updateValues.put(GroupMembership.GROUP_ROW_ID, id);
updateArgs[0] = Long.toString(id);
cr.update(ContactsContract.Data.CONTENT_URI, updateValues,
MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
}
// Really delete groups that are marked deleted
cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
Groups.DELETED + "=1", null);
// Clear the dirty flag for all of our groups
updateValues.clear();
updateValues.put(Groups.DIRTY, 0);
cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI, emailAddress),
updateValues, null, null);
}
} finally {
c.close();
}
}
/**
* Helper to add a string to the upsync.
* @param s The {@link Serializer} for this sync request
* @param cv The {@link ContentValues} with the data for this string.
* @param column The column name in cv to find the string.
* @param tag The tag to use when adding to s.
* @throws IOException
*/
private static void sendStringData(final Serializer s, final ContentValues cv,
final String column, final int tag) throws IOException {
if (cv.containsKey(column)) {
final String value = cv.getAsString(column);
if (!TextUtils.isEmpty(value)) {
s.data(tag, value);
}
}
}
// This is to catch when the contacts provider has a date in this particular wrong format.
private static final SimpleDateFormat SHORT_DATE_FORMAT;
// Array of formats we check when parsing dates from the contacts provider.
private static final DateFormat[] DATE_FORMATS;
static {
SHORT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
SHORT_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
//TODO: We only handle two formatting types. The default contacts app will work with this
// but any other contacts apps might not. We can try harder to handle those guys too.
DATE_FORMATS = new DateFormat[] { Eas.DATE_FORMAT, SHORT_DATE_FORMAT };
}
/**
* Helper to add a date to the upsync. It reads the date as a string from the
* {@link ContentValues} that we got from the provider, tries to parse it using various formats,
* and formats it correctly to send to the server. If it can't parse it, it will omit the date
* in the upsync; since Birthdays (the only date currently supported by this class) can be
* ghosted, this means that any date changes on the client will NOT be reflected on the server.
* @param s The {@link Serializer} for this sync request
* @param cv The {@link ContentValues} with the data for this string.
* @param column The column name in cv to find the string.
* @param tag The tag to use when adding to s.
* @throws IOException
*/
private static void sendDateData(final Serializer s, final ContentValues cv,
final String column, final int tag) throws IOException {
if (cv.containsKey(column)) {
final String value = cv.getAsString(column);
if (!TextUtils.isEmpty(value)) {
Date date;
// Check all the formats we know about to see if one of them works.
for (final DateFormat format : DATE_FORMATS) {
try {
date = format.parse(value);
if (date != null) {
// We got a legit date for this format, so send it up.
s.data(tag, Eas.DATE_FORMAT.format(date));
return;
}
} catch (final ParseException e) {
// The date didn't match this particular format; keep looping.
}
}
}
}
}
/**
* Add a nickname to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this nickname.
* @throws IOException
*/
private static void sendNickname(final Serializer s, final ContentValues cv)
throws IOException {
sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
}
/**
* Add children data to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for a set of children.
* @throws IOException
*/
private static void sendChildren(final Serializer s, final ContentValues cv)
throws IOException {
boolean first = true;
for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
final String row = EasChildren.ROWS[i];
if (cv.containsKey(row)) {
if (first) {
s.start(Tags.CONTACTS_CHILDREN);
first = false;
}
s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
}
}
if (!first) {
s.end();
}
}
/**
* Add business contact info to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this business contact.
* @throws IOException
*/
private static void sendBusiness(final Serializer s, final ContentValues cv)
throws IOException {
sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID);
}
/**
* Add a webpage info to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this webpage.
* @throws IOException
*/
private static void sendWebpage(final Serializer s, final ContentValues cv) throws IOException {
sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
}
/**
* Add personal contact info to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this personal contact.
* @throws IOException
*/
private static void sendPersonal(final Serializer s, final ContentValues cv)
throws IOException {
sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
}
/**
* Add a phone number to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this phone number.
* @param workCount The number of work phone numbers already added.
* @param homeCount The number of home phone numbers already added.
* @throws IOException
*/
private static void sendPhone(final Serializer s, final ContentValues cv, final int workCount,
final int homeCount) throws IOException {
final String value = cv.getAsString(Phone.NUMBER);
if (value == null) return;
switch (cv.getAsInteger(Phone.TYPE)) {
case Phone.TYPE_WORK:
if (workCount < MAX_PHONE_ROWS) {
s.data(WORK_PHONE_TAGS[workCount], value);
}
break;
case Phone.TYPE_MMS:
s.data(Tags.CONTACTS2_MMS, value);
break;
case Phone.TYPE_ASSISTANT:
s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
break;
case Phone.TYPE_FAX_WORK:
s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
break;
case Phone.TYPE_COMPANY_MAIN:
s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
break;
case Phone.TYPE_HOME:
if (homeCount < MAX_PHONE_ROWS) {
s.data(HOME_PHONE_TAGS[homeCount], value);
}
break;
case Phone.TYPE_MOBILE:
s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
break;
case Phone.TYPE_CAR:
s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
break;
case Phone.TYPE_PAGER:
s.data(Tags.CONTACTS_PAGER_NUMBER, value);
break;
case Phone.TYPE_RADIO:
s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
break;
case Phone.TYPE_FAX_HOME:
s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
break;
default:
break;
}
}
/**
* Add a relation to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this relation.
* @throws IOException
*/
private static void sendRelation(final Serializer s, final ContentValues cv)
throws IOException {
final String value = cv.getAsString(Relation.DATA);
if (value == null) return;
switch (cv.getAsInteger(Relation.TYPE)) {
case Relation.TYPE_ASSISTANT:
s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
break;
case Relation.TYPE_MANAGER:
s.data(Tags.CONTACTS2_MANAGER_NAME, value);
break;
case Relation.TYPE_SPOUSE:
s.data(Tags.CONTACTS_SPOUSE, value);
break;
default:
break;
}
}
/**
* Add a name to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this name.
* @throws IOException
*/
// TODO: This used to return a displayName, but it was always null. Figure out what it really
// wanted to return.
private static void sendStructuredName(final Serializer s, final ContentValues cv)
throws IOException {
sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
}
/**
* Add an address of a particular type to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this address.
* @param fieldNames The field names for this address type.
* @throws IOException
*/
private static void sendOnePostal(final Serializer s, final ContentValues cv,
final int[] fieldNames) throws IOException{
sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
}
/**
* Add an address to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this address.
* @throws IOException
*/
private static void sendStructuredPostal(final Serializer s, final ContentValues cv)
throws IOException {
switch (cv.getAsInteger(StructuredPostal.TYPE)) {
case StructuredPostal.TYPE_HOME:
sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
break;
case StructuredPostal.TYPE_WORK:
sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
break;
case StructuredPostal.TYPE_OTHER:
sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
break;
default:
break;
}
}
/**
* Add an organization to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this organization.
* @throws IOException
*/
private static void sendOrganization(final Serializer s, final ContentValues cv)
throws IOException {
sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION);
}
/**
* Add an IM to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this IM.
* @throws IOException
*/
private static void sendIm(final Serializer s, final ContentValues cv, final int count)
throws IOException {
final String value = cv.getAsString(Im.DATA);
if (value == null) return;
if (count < MAX_IM_ROWS) {
s.data(IM_TAGS[count], value);
}
}
/**
* Add a birthday to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this birthday.
* @throws IOException
*/
private static void sendBirthday(final Serializer s, final ContentValues cv)
throws IOException {
sendDateData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
}
/**
* Add a note to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this note.
* @param protocolVersion
* @throws IOException
*/
private void sendNote(final Serializer s, final ContentValues cv, final double protocolVersion)
throws IOException {
// Even when there is no local note, we must explicitly upsync an empty note,
// which is the only way to force the server to delete any pre-existing note.
String note = "";
if (cv.containsKey(Note.NOTE)) {
// EAS won't accept note data with raw newline characters
note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
}
// Format of upsync data depends on protocol version
if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
s.start(Tags.BASE_BODY);
s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
s.end();
} else {
s.data(Tags.CONTACTS_BODY, note);
}
}
/**
* Add a photo to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this photo.
* @throws IOException
*/
private static void sendPhoto(final Serializer s, final ContentValues cv) throws IOException {
if (cv.containsKey(Photo.PHOTO)) {
final byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
final String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
s.data(Tags.CONTACTS_PICTURE, pic);
} else {
// Send an empty tag, which signals the server to delete any pre-existing photo
s.tag(Tags.CONTACTS_PICTURE);
}
}
/**
* Add an email address to the upsync.
* @param s The {@link Serializer} for this sync request.
* @param cv The {@link ContentValues} with the data for this email address.
* @param count The number of email addresses that have already been added.
* @param displayName The display name for this contact.
* @param protocolVersion
* @throws IOException
*/
private void sendEmail(final Serializer s, final ContentValues cv, final int count,
final String displayName, final double protocolVersion) throws IOException {
// Get both parts of the email address (a newly created one in the UI won't have a name)
final String addr = cv.getAsString(Email.DATA);
String name = cv.getAsString(Email.DISPLAY_NAME);
if (name == null) {
if (displayName != null) {
name = displayName;
} else {
name = addr;
}
}
// Compose address from name and addr
if (addr != null) {
final String value;
// Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on
// an RFC822 address)
if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
value = addr;
} else {
value = '\"' + name + "\" <" + addr + '>';
}
if (count < MAX_EMAIL_ROWS) {
s.data(EMAIL_TAGS[count], value);
}
}
}
private void setUpsyncCommands(final Serializer s, final ContentResolver cr,
final Account account, final Mailbox mailbox, final double protocolVersion)
throws IOException {
// Find any groups of ours that are dirty and dirty those groups' members
dirtyContactsWithinDirtyGroups(cr, account);
// First, let's find Contacts that have changed.
final Uri uri = uriWithAccountAndIsSyncAdapter(
ContactsContract.RawContactsEntity.CONTENT_URI, account.mEmailAddress);
// Get them all atomically
final EntityIterator ei = ContactsContract.RawContacts.newEntityIterator(
cr.query(uri, null, ContactsContract.RawContacts.DIRTY + "=1", null, null));
final ContentValues cidValues = new ContentValues();
try {
boolean first = true;
final Uri rawContactUri = addCallerIsSyncAdapterParameter(
ContactsContract.RawContacts.CONTENT_URI);
while (ei.hasNext()) {
final Entity entity = ei.next();
// For each of these entities, create the change commands
final ContentValues entityValues = entity.getEntityValues();
final String serverId =
entityValues.getAsString(ContactsContract.RawContacts.SOURCE_ID);
final ArrayList<Integer> groupIds = new ArrayList<Integer>();
if (first) {
s.start(Tags.SYNC_COMMANDS);
LogUtils.d(TAG, "Sending Contacts changes to the server");
first = false;
}
if (serverId == null) {
// This is a new contact; create a clientId
final String clientId =
"new_" + mailbox.mId + '_' + System.currentTimeMillis();
LogUtils.d(TAG, "Creating new contact with clientId: %s", clientId);
s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
// And save it in the raw contact
cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
cr.update(ContentUris.withAppendedId(rawContactUri,
entityValues.getAsLong(ContactsContract.RawContacts._ID)),
cidValues, null, null);
} else {
if (entityValues.getAsInteger(ContactsContract.RawContacts.DELETED) == 1) {
LogUtils.d(TAG, "Deleting contact with serverId: %s", serverId);
s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
mDeletedContacts.add(
entityValues.getAsLong(ContactsContract.RawContacts._ID));
continue;
}
LogUtils.d(TAG, "Upsync change to contact with serverId: %s", serverId);
s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
}
s.start(Tags.SYNC_APPLICATION_DATA);
// Write out the data here
int imCount = 0;
int emailCount = 0;
int homePhoneCount = 0;
int workPhoneCount = 0;
// TODO: How is this name supposed to be formed?
String displayName = null;
final ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
final ContentValues cv = ncv.values;
final String mimeType = cv.getAsString(ContactsContract.Data.MIMETYPE);
if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
emailValues.add(cv);
} else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
sendNickname(s, cv);
} else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
sendChildren(s, cv);
} else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
sendBusiness(s, cv);
} else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
sendWebpage(s, cv);
} else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
sendPersonal(s, cv);
} else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
sendPhone(s, cv, workPhoneCount, homePhoneCount);
int type = cv.getAsInteger(Phone.TYPE);
if (type == Phone.TYPE_HOME) homePhoneCount++;
if (type == Phone.TYPE_WORK) workPhoneCount++;
} else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
sendRelation(s, cv);
} else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
sendStructuredName(s, cv);
} else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
sendStructuredPostal(s, cv);
} else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
sendOrganization(s, cv);
} else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
sendIm(s, cv, imCount++);
} else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
Integer eventType = cv.getAsInteger(Event.TYPE);
if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
sendBirthday(s, cv);
}
} else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
// We must gather these, and send them together (below)
groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
} else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
sendNote(s, cv, protocolVersion);
} else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
sendPhoto(s, cv);
} else {
LogUtils.i(TAG, "Contacts upsync, unknown data: %s", mimeType);
}
}
// We do the email rows last, because we need to make sure we've found the
// displayName (if one exists); this would be in a StructuredName rnow
for (final ContentValues cv: emailValues) {
sendEmail(s, cv, emailCount++, displayName, protocolVersion);
}
// Now, we'll send up groups, if any
if (!groupIds.isEmpty()) {
boolean groupFirst = true;
for (final int id: groupIds) {
// Since we get id's from the provider, we need to find their names
final Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI,
id), GROUP_TITLE_PROJECTION, null, null, null);
try {
// Presumably, this should always succeed, but ...
if (c.moveToFirst()) {
if (groupFirst) {
s.start(Tags.CONTACTS_CATEGORIES);
groupFirst = false;
}
s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
}
} finally {
c.close();
}
}
if (!groupFirst) {
s.end();
}
}
s.end().end(); // ApplicationData & Change
mUpdatedContacts.add(entityValues.getAsLong(ContactsContract.RawContacts._ID));
}
if (!first) {
s.end(); // Commands
}
} finally {
ei.close();
}
}
@Override
public void cleanup(final Context context, final Account account) {
final ContentResolver cr = context.getContentResolver();
// Mark the changed contacts dirty = 0
// Permanently delete the user deletions
ContactsSyncParser.ContactOperations ops = new ContactsSyncParser.ContactOperations();
for (final Long id: mUpdatedContacts) {
ops.add(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
id).buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build())
.withValue(ContactsContract.RawContacts.DIRTY, 0).build());
}
for (final Long id: mDeletedContacts) {
ops.add(ContentProviderOperation.newDelete(ContentUris.withAppendedId(
ContactsContract.RawContacts.CONTENT_URI, id).buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build())
.build());
}
ops.execute(context);
if (mParser != null && mParser.isGroupsUsed()) {
// Make sure the title column is set for all of our groups
// And that all of our groups are visible
// TODO Perhaps the visible part should only happen when the group is created, but
// this is fine for now.
final Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI,
account.mEmailAddress);
final Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
Groups.TITLE + " IS NULL", null, null);
final ContentValues values = new ContentValues();
values.put(Groups.GROUP_VISIBLE, 1);
try {
while (c.moveToNext()) {
final String sourceId = c.getString(0);
values.put(Groups.TITLE, sourceId);
cr.update(uriWithAccountAndIsSyncAdapter(groupsUri,
account.mEmailAddress), values, Groups.SOURCE_ID + "=?",
new String[] {sourceId});
}
} finally {
c.close();
}
}
}
/**
* Delete an account from the Contacts provider.
* @param context Our {@link Context}
* @param emailAddress The email address of the account we wish to delete
*/
public static void wipeAccountFromContentProvider(final Context context,
final String emailAddress) {
context.getContentResolver().delete(uriWithAccountAndIsSyncAdapter(
ContactsContract.RawContacts.CONTENT_URI, emailAddress), null, null);
}
}