blob: b0d4d09c0d214dfa3724586bfd1cfd4bc8701322 [file] [log] [blame]
package com.android.exchange.adapter;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.provider.CalendarContract;
import android.provider.SyncStateContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.ExtendedProperties;
import android.provider.CalendarContract.Reminders;
import android.provider.CalendarContract.SyncState;
import android.text.format.DateUtils;
import com.android.emailcommon.Logging;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.Eas;
import com.android.exchange.adapter.AbstractSyncAdapter.Operation;
import com.android.exchange.utility.CalendarUtilities;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.Map.Entry;
public class CalendarSyncParser extends AbstractSyncParser {
private static final String TAG = Logging.LOG_TAG;
private final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
private final TimeZone mLocalTimeZone = TimeZone.getDefault();
private final long mCalendarId;
private final android.accounts.Account mAccountManagerAccount;
private final Uri mAsSyncAdapterAttendees;
private final Uri mAsSyncAdapterEvents;
private final String[] mBindArgument = new String[1];
private final CalendarOperations mOps;
private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
// Since exceptions will have the same _SYNC_ID as the original event we have to check that
// there's no original event when finding an item by _SYNC_ID
private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " +
Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?";
private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
private static final String[] ID_PROJECTION = new String[] {Events._ID};
private static final String EVENT_ID_AND_NAME =
ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?";
private static final String[] EXTENDED_PROPERTY_PROJECTION =
new String[] {ExtendedProperties._ID};
private static final int EXTENDED_PROPERTY_ID = 0;
private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp";
private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status";
private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
// Used to indicate that we removed the attendee list because it was too large
private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted";
// Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges)
private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited";
private static final Operation PLACEHOLDER_OPERATION =
new Operation(ContentProviderOperation.newInsert(Uri.EMPTY));
private static final long SEPARATOR_ID = Long.MAX_VALUE;
// Maximum number of allowed attendees; above this number, we mark the Event with the
// attendeesRedacted extended property and don't allow the event to be upsynced to the server
private static final int MAX_SYNCED_ATTENDEES = 50;
// We set the organizer to this when the user is the organizer and we've redacted the
// attendee list. By making the meeting organizer OTHER than the user, we cause the UI to
// prevent edits to this event (except local changes like reminder).
private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa";
// Maximum number of CPO's before we start redacting attendees in exceptions
// The number 500 has been determined empirically; 1500 CPOs appears to be the limit before
// binder failures occur, but we need room at any point for additional events/exceptions so
// we set our limit at 1/3 of the apparent maximum for extra safety
// TODO Find a better solution to this workaround
private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500;
public CalendarSyncParser(final Context context, final ContentResolver resolver,
final InputStream in, final Mailbox mailbox, final Account account,
final android.accounts.Account accountManagerAccount,
final long calendarId) throws IOException {
super(context, resolver, in, mailbox, account);
mAccountManagerAccount = accountManagerAccount;
mCalendarId = calendarId;
mAsSyncAdapterAttendees = asSyncAdapter(Attendees.CONTENT_URI,
mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
mAsSyncAdapterEvents = asSyncAdapter(Events.CONTENT_URI,
mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
mOps = new CalendarOperations(resolver, mAsSyncAdapterAttendees, mAsSyncAdapterEvents,
asSyncAdapter(Reminders.CONTENT_URI, mAccount.mEmailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
asSyncAdapter(ExtendedProperties.CONTENT_URI, mAccount.mEmailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE));
}
protected static class CalendarOperations extends ArrayList<Operation> {
private static final long serialVersionUID = 1L;
public int mCount = 0;
private int mEventStart = 0;
private final ContentResolver mContentResolver;
private final Uri mAsSyncAdapterAttendees;
private final Uri mAsSyncAdapterEvents;
private final Uri mAsSyncAdapterReminders;
private final Uri mAsSyncAdapterExtendedProperties;
public CalendarOperations(final ContentResolver contentResolver,
final Uri asSyncAdapterAttendees, final Uri asSyncAdapterEvents,
final Uri asSyncAdapterReminders, final Uri asSyncAdapterExtendedProperties) {
mContentResolver = contentResolver;
mAsSyncAdapterAttendees = asSyncAdapterAttendees;
mAsSyncAdapterEvents = asSyncAdapterEvents;
mAsSyncAdapterReminders = asSyncAdapterReminders;
mAsSyncAdapterExtendedProperties = asSyncAdapterExtendedProperties;
}
@Override
public boolean add(Operation op) {
super.add(op);
mCount++;
return true;
}
public int newEvent(Operation op) {
mEventStart = mCount;
add(op);
return mEventStart;
}
public int newDelete(long id, String serverId) {
int offset = mCount;
delete(id, serverId);
return offset;
}
public void newAttendee(ContentValues cv) {
newAttendee(cv, mEventStart);
}
public void newAttendee(ContentValues cv, int eventStart) {
add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
.withValues(cv),
Attendees.EVENT_ID,
eventStart));
}
public void updatedAttendee(ContentValues cv, long id) {
cv.put(Attendees.EVENT_ID, id);
add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
.withValues(cv)));
}
public void newException(ContentValues cv) {
add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents)
.withValues(cv)));
}
public void newExtendedProperty(String name, String value) {
add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties)
.withValue(ExtendedProperties.NAME, name)
.withValue(ExtendedProperties.VALUE, value),
ExtendedProperties.EVENT_ID,
mEventStart));
}
public void updatedExtendedProperty(String name, String value, long id) {
// Find an existing ExtendedProperties row for this event and property name
Cursor c = mContentResolver.query(ExtendedProperties.CONTENT_URI,
EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME,
new String[] {Long.toString(id), name}, null);
long extendedPropertyId = -1;
// If there is one, capture its _id
if (c != null) {
try {
if (c.moveToFirst()) {
extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID);
}
} finally {
c.close();
}
}
// Either do an update or an insert, depending on whether one
// already exists
if (extendedPropertyId >= 0) {
add(new Operation(ContentProviderOperation
.newUpdate(
ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties,
extendedPropertyId))
.withValue(ExtendedProperties.VALUE, value)));
} else {
newExtendedProperty(name, value);
}
}
public void newReminder(int mins, int eventStart) {
add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders)
.withValue(Reminders.MINUTES, mins)
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT),
ExtendedProperties.EVENT_ID,
eventStart));
}
public void newReminder(int mins) {
newReminder(mins, mEventStart);
}
public void delete(long id, String syncId) {
add(new Operation(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(mAsSyncAdapterEvents, id))));
// Delete the exceptions for this Event (CalendarProvider doesn't do this)
add(new Operation(ContentProviderOperation
.newDelete(mAsSyncAdapterEvents)
.withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId})));
}
}
private static Uri asSyncAdapter(Uri uri, String account, String accountType) {
return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, account)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
}
private static void addOrganizerToAttendees(CalendarOperations ops, long eventId,
String organizerName, String organizerEmail) {
// Handle the organizer (who IS an attendee on device, but NOT in EAS)
if (organizerName != null || organizerEmail != null) {
ContentValues attendeeCv = new ContentValues();
if (organizerName != null) {
attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName);
}
if (organizerEmail != null) {
attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
}
attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
if (eventId < 0) {
ops.newAttendee(attendeeCv);
} else {
ops.updatedAttendee(attendeeCv, eventId);
}
}
}
/**
* Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event
* The follow rules are enforced by CalendarProvider2:
* Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION
* Recurring events (i.e. events with RRULE) must have a DURATION
* All-day recurring events MUST have a DURATION that is in the form P<n>D
* Other events MAY have a DURATION in any valid form (we use P<n>M)
* All-day events MUST have hour, minute, and second = 0; in addition, they must have
* the EVENT_TIMEZONE set to UTC
* Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has
* hour, minute, and second = 0 and be set in UTC
* @param cv the ContentValues for the Event
* @param startTime the start time for the Event
* @param endTime the end time for the Event
* @param allDayEvent whether this is an all day event (1) or not (0)
*/
/*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime,
int allDayEvent) {
// If there's no startTime, the event will be found to be invalid, so return
if (startTime < 0) return;
// EAS events can arrive without an end time, but CalendarProvider requires them
// so we'll default to 30 minutes; this will be superceded if this is an all-day event
if (endTime < 0) endTime = startTime + (30 * DateUtils.MINUTE_IN_MILLIS);
// If this is an all-day event, set hour, minute, and second to zero, and use UTC
if (allDayEvent != 0) {
startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone);
endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone);
String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE);
cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone);
cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
}
// If this is an exception, and the original was an all-day event, make sure the
// original instance time has hour, minute, and second set to zero, and is in UTC
if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) &&
cv.containsKey(Events.ORIGINAL_ALL_DAY)) {
Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY);
if (ade != null && ade != 0) {
long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
final GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
exceptionTime = CalendarUtilities.getUtcAllDayCalendarTime(exceptionTime,
mLocalTimeZone);
cal.setTimeInMillis(exceptionTime);
cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
cal.set(GregorianCalendar.MINUTE, 0);
cal.set(GregorianCalendar.SECOND, 0);
cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis());
}
}
// Always set DTSTART
cv.put(Events.DTSTART, startTime);
// For recurring events, set DURATION. Use P<n>D format for all day events
if (cv.containsKey(Events.RRULE)) {
if (allDayEvent != 0) {
cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.DAY_IN_MILLIS) + "D");
}
else {
cv.put(Events.DURATION, "P" + ((endTime - startTime) / DateUtils.MINUTE_IN_MILLIS) + "M");
}
// For other events, set DTEND and LAST_DATE
} else {
cv.put(Events.DTEND, endTime);
cv.put(Events.LAST_DATE, endTime);
}
}
public void addEvent(CalendarOperations ops, String serverId, boolean update)
throws IOException {
ContentValues cv = new ContentValues();
cv.put(Events.CALENDAR_ID, mCalendarId);
cv.put(Events._SYNC_ID, serverId);
cv.put(Events.HAS_ATTENDEE_DATA, 1);
cv.put(Events.SYNC_DATA2, "0");
int allDayEvent = 0;
String organizerName = null;
String organizerEmail = null;
int eventOffset = -1;
int deleteOffset = -1;
int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
int responseType = CalendarUtilities.RESPONSE_TYPE_NONE;
boolean firstTag = true;
long eventId = -1;
long startTime = -1;
long endTime = -1;
TimeZone timeZone = null;
// Keep track of the attendees; exceptions will need them
ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
int reminderMins = -1;
String dtStamp = null;
boolean organizerAdded = false;
while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
if (update && firstTag) {
// Find the event that's being updated
Cursor c = getServerIdCursor(serverId);
long id = -1;
try {
if (c != null && c.moveToFirst()) {
id = c.getLong(0);
}
} finally {
if (c != null) c.close();
}
if (id > 0) {
// DTSTAMP can come first, and we simply need to track it
if (tag == Tags.CALENDAR_DTSTAMP) {
dtStamp = getValue();
continue;
} else if (tag == Tags.CALENDAR_ATTENDEES) {
// This is an attendees-only update; just
// delete/re-add attendees
mBindArgument[0] = Long.toString(id);
ops.add(new Operation(ContentProviderOperation
.newDelete(mAsSyncAdapterAttendees)
.withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)));
eventId = id;
} else {
// Otherwise, delete the original event and recreate it
userLog("Changing (delete/add) event ", serverId);
deleteOffset = ops.newDelete(id, serverId);
// Add a placeholder event so that associated tables can reference
// this as a back reference. We add the event at the end of the method
eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
}
} else {
// The changed item isn't found. We'll treat this as a new item
eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
userLog(TAG, "Changed item not found; treating as new.");
}
} else if (firstTag) {
// Add a placeholder event so that associated tables can reference
// this as a back reference. We add the event at the end of the method
eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
}
firstTag = false;
switch (tag) {
case Tags.CALENDAR_ALL_DAY_EVENT:
allDayEvent = getValueInt();
if (allDayEvent != 0 && timeZone != null) {
// If the event doesn't start at midnight local time, we won't consider
// this an all-day event in the local time zone (this is what OWA does)
GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone);
cal.setTimeInMillis(startTime);
userLog("All-day event arrived in: " + timeZone.getID());
if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 ||
cal.get(GregorianCalendar.MINUTE) != 0) {
allDayEvent = 0;
userLog("Not an all-day event locally: " + mLocalTimeZone.getID());
}
}
cv.put(Events.ALL_DAY, allDayEvent);
break;
case Tags.CALENDAR_ATTACHMENTS:
attachmentsParser();
break;
case Tags.CALENDAR_ATTENDEES:
// If eventId >= 0, this is an update; otherwise, a new Event
attendeeValues = attendeesParser();
break;
case Tags.BASE_BODY:
cv.put(Events.DESCRIPTION, bodyParser());
break;
case Tags.CALENDAR_BODY:
cv.put(Events.DESCRIPTION, getValue());
break;
case Tags.CALENDAR_TIME_ZONE:
timeZone = CalendarUtilities.tziStringToTimeZone(getValue());
if (timeZone == null) {
timeZone = mLocalTimeZone;
}
cv.put(Events.EVENT_TIMEZONE, timeZone.getID());
break;
case Tags.CALENDAR_START_TIME:
startTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_END_TIME:
endTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_EXCEPTIONS:
// For exceptions to show the organizer, the organizer must be added before
// we call exceptionsParser
addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
organizerAdded = true;
exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus,
startTime, endTime);
break;
case Tags.CALENDAR_LOCATION:
cv.put(Events.EVENT_LOCATION, getValue());
break;
case Tags.CALENDAR_RECURRENCE:
String rrule = recurrenceParser();
if (rrule != null) {
cv.put(Events.RRULE, rrule);
}
break;
case Tags.CALENDAR_ORGANIZER_EMAIL:
organizerEmail = getValue();
cv.put(Events.ORGANIZER, organizerEmail);
break;
case Tags.CALENDAR_SUBJECT:
cv.put(Events.TITLE, getValue());
break;
case Tags.CALENDAR_SENSITIVITY:
cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
break;
case Tags.CALENDAR_ORGANIZER_NAME:
organizerName = getValue();
break;
case Tags.CALENDAR_REMINDER_MINS_BEFORE:
// Save away whether this tag has content; Exchange 2010 sends an empty tag
// rather than not sending one (as with Ex07 and Ex03)
boolean hasContent = !noContent;
reminderMins = getValueInt();
if (hasContent) {
ops.newReminder(reminderMins);
cv.put(Events.HAS_ALARM, 1);
}
break;
// The following are fields we should save (for changes), though they don't
// relate to data used by CalendarProvider at this point
case Tags.CALENDAR_UID:
cv.put(Events.SYNC_DATA2, getValue());
break;
case Tags.CALENDAR_DTSTAMP:
dtStamp = getValue();
break;
case Tags.CALENDAR_MEETING_STATUS:
ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue());
break;
case Tags.CALENDAR_BUSY_STATUS:
// We'll set the user's status in the Attendees table below
// Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
// attendee!
busyStatus = getValueInt();
break;
case Tags.CALENDAR_RESPONSE_TYPE:
// EAS 14+ uses this for the user's response status; we'll use this instead
// of busy status, if it appears
responseType = getValueInt();
break;
case Tags.CALENDAR_CATEGORIES:
String categories = categoriesParser();
if (categories.length() > 0) {
ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories);
}
break;
default:
skipTag();
}
}
// Enforce CalendarProvider required properties
setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
// Set user's availability
cv.put(Events.AVAILABILITY, CalendarUtilities.availabilityFromBusyStatus(busyStatus));
// If we haven't added the organizer to attendees, do it now
if (!organizerAdded) {
addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
}
// Note that organizerEmail can be null with a DTSTAMP only change from the server
boolean selfOrganizer = (mAccount.mEmailAddress.equals(organizerEmail));
// Store email addresses of attendees (in a tokenizable string) in ExtendedProperties
// If the user is an attendee, set the attendee status using busyStatus (note that the
// busyStatus is inherited from the parent unless it's specified in the exception)
// Add the insert/update operation for each attendee (based on whether it's add/change)
int numAttendees = attendeeValues.size();
if (numAttendees > MAX_SYNCED_ATTENDEES) {
// Indicate that we've redacted attendees. If we're the organizer, disable edit
// by setting organizerEmail to a bogus value and by setting the upsync prohibited
// extended properly.
// Note that we don't set ANY attendees if we're in this branch; however, the
// organizer has already been included above, and WILL show up (which is good)
if (eventId < 0) {
ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1");
if (selfOrganizer) {
ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1");
}
} else {
ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId);
if (selfOrganizer) {
ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1",
eventId);
}
}
if (selfOrganizer) {
organizerEmail = BOGUS_ORGANIZER_EMAIL;
cv.put(Events.ORGANIZER, organizerEmail);
}
// Tell UI that we don't have any attendees
cv.put(Events.HAS_ATTENDEE_DATA, "0");
LogUtils.i(TAG, "Maximum number of attendees exceeded; redacting");
} else if (numAttendees > 0) {
StringBuilder sb = new StringBuilder();
for (ContentValues attendee: attendeeValues) {
String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL);
sb.append(attendeeEmail);
sb.append(ATTENDEE_TOKENIZER_DELIMITER);
if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
int attendeeStatus;
// We'll use the response type (EAS 14), if we've got one; otherwise, we'll
// try to infer it from busy status
if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) {
attendeeStatus =
CalendarUtilities.attendeeStatusFromResponseType(responseType);
} else if (!update) {
// For new events in EAS < 14, we have no idea what the busy status
// means, so we show "none", allowing the user to select an option.
attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
} else {
// For updated events, we'll try to infer the attendee status from the
// busy status
attendeeStatus =
CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus);
}
attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus);
// If we're an attendee, save away our initial attendee status in the
// event's ExtendedProperties (we look for differences between this and
// the user's current attendee status to determine whether an email needs
// to be sent to the organizer)
// organizerEmail will be null in the case that this is an attendees-only
// change from the server
if (organizerEmail == null ||
!organizerEmail.equalsIgnoreCase(attendeeEmail)) {
if (eventId < 0) {
ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
Integer.toString(attendeeStatus));
} else {
ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
Integer.toString(attendeeStatus), eventId);
}
}
}
if (eventId < 0) {
ops.newAttendee(attendee);
} else {
ops.updatedAttendee(attendee, eventId);
}
}
if (eventId < 0) {
ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString());
ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0");
ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0");
} else {
ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(),
eventId);
ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId);
ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId);
}
}
// Put the real event in the proper place in the ops ArrayList
if (eventOffset >= 0) {
// Store away the DTSTAMP here
if (dtStamp != null) {
ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp);
}
if (isValidEventValues(cv)) {
ops.set(eventOffset,
new Operation(ContentProviderOperation
.newInsert(mAsSyncAdapterEvents).withValues(cv)));
} else {
// If we can't add this event (it's invalid), remove all of the inserts
// we've built for it
int cnt = ops.mCount - eventOffset;
userLog(TAG, "Removing " + cnt + " inserts from mOps");
for (int i = 0; i < cnt; i++) {
ops.remove(eventOffset);
}
ops.mCount = eventOffset;
// If this is a change, we need to also remove the deletion that comes
// before the addition
if (deleteOffset >= 0) {
// Remove the deletion
ops.remove(deleteOffset);
// And the deletion of exceptions
ops.remove(deleteOffset);
userLog(TAG, "Removing deletion ops from mOps");
ops.mCount = deleteOffset;
}
}
}
// Mark the end of the event
addSeparatorOperation(ops, Events.CONTENT_URI);
}
private void logEventColumns(ContentValues cv, String reason) {
if (Eas.USER_LOG) {
StringBuilder sb =
new StringBuilder("Event invalid, " + reason + ", skipping: Columns = ");
for (Entry<String, Object> entry: cv.valueSet()) {
sb.append(entry.getKey());
sb.append('/');
}
userLog(TAG, sb.toString());
}
}
/*package*/ boolean isValidEventValues(ContentValues cv) {
boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME);
// All events require DTSTART
if (!cv.containsKey(Events.DTSTART)) {
logEventColumns(cv, "DTSTART missing");
return false;
// If we're a top-level event, we must have _SYNC_DATA (uid)
} else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) {
logEventColumns(cv, "_SYNC_DATA missing");
return false;
// We must also have DTEND or DURATION if we're not an exception
} else if (!isException && !cv.containsKey(Events.DTEND) &&
!cv.containsKey(Events.DURATION)) {
logEventColumns(cv, "DTEND/DURATION missing");
return false;
// Exceptions require DTEND
} else if (isException && !cv.containsKey(Events.DTEND)) {
logEventColumns(cv, "Exception missing DTEND");
return false;
// If this is a recurrence, we need a DURATION (in days if an all-day event)
} else if (cv.containsKey(Events.RRULE)) {
String duration = cv.getAsString(Events.DURATION);
if (duration == null) return false;
if (cv.containsKey(Events.ALL_DAY)) {
Integer ade = cv.getAsInteger(Events.ALL_DAY);
if (ade != null && ade != 0 && !duration.endsWith("D")) {
return false;
}
}
}
return true;
}
public String recurrenceParser() throws IOException {
// Turn this information into an RRULE
int type = -1;
int occurrences = -1;
int interval = -1;
int dow = -1;
int dom = -1;
int wom = -1;
int moy = -1;
String until = null;
while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
switch (tag) {
case Tags.CALENDAR_RECURRENCE_TYPE:
type = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_INTERVAL:
interval = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
occurrences = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
dow = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
dom = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
wom = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
moy = getValueInt();
break;
case Tags.CALENDAR_RECURRENCE_UNTIL:
until = getValue();
break;
default:
skipTag();
}
}
return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
dow, dom, wom, moy, until);
}
private void exceptionParser(CalendarOperations ops, ContentValues parentCv,
ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
long startTime, long endTime) throws IOException {
ContentValues cv = new ContentValues();
cv.put(Events.CALENDAR_ID, mCalendarId);
// It appears that these values have to be copied from the parent if they are to appear
// Note that they can be overridden below
cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION));
cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION));
cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL));
cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
// Exceptions should always have this set to zero, since EAS has no concept of
// separate attendee lists for exceptions; if we fail to do this, then the UI will
// allow the user to change attendee data, and this change would never get reflected
// on the server.
cv.put(Events.HAS_ATTENDEE_DATA, 0);
int allDayEvent = 0;
// This column is the key that links the exception to the serverId
cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID));
String exceptionStartTime = "_noStartTime";
while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
switch (tag) {
case Tags.CALENDAR_ATTACHMENTS:
attachmentsParser();
break;
case Tags.CALENDAR_EXCEPTION_START_TIME:
exceptionStartTime = getValue();
cv.put(Events.ORIGINAL_INSTANCE_TIME,
Utility.parseDateTimeToMillis(exceptionStartTime));
break;
case Tags.CALENDAR_EXCEPTION_IS_DELETED:
if (getValueInt() == 1) {
cv.put(Events.STATUS, Events.STATUS_CANCELED);
}
break;
case Tags.CALENDAR_ALL_DAY_EVENT:
allDayEvent = getValueInt();
cv.put(Events.ALL_DAY, allDayEvent);
break;
case Tags.BASE_BODY:
cv.put(Events.DESCRIPTION, bodyParser());
break;
case Tags.CALENDAR_BODY:
cv.put(Events.DESCRIPTION, getValue());
break;
case Tags.CALENDAR_START_TIME:
startTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_END_TIME:
endTime = Utility.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_LOCATION:
cv.put(Events.EVENT_LOCATION, getValue());
break;
case Tags.CALENDAR_RECURRENCE:
String rrule = recurrenceParser();
if (rrule != null) {
cv.put(Events.RRULE, rrule);
}
break;
case Tags.CALENDAR_SUBJECT:
cv.put(Events.TITLE, getValue());
break;
case Tags.CALENDAR_SENSITIVITY:
cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
break;
case Tags.CALENDAR_BUSY_STATUS:
busyStatus = getValueInt();
// Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
// attendee!
break;
// TODO How to handle these items that are linked to event id!
// case Tags.CALENDAR_DTSTAMP:
// ops.newExtendedProperty("dtstamp", getValue());
// break;
// case Tags.CALENDAR_REMINDER_MINS_BEFORE:
// ops.newReminder(getValueInt());
// break;
default:
skipTag();
}
}
// We need a _sync_id, but it can't be the parent's id, so we generate one
cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
exceptionStartTime);
// Enforce CalendarProvider required properties
setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
// Don't insert an invalid exception event
if (!isValidEventValues(cv)) return;
// Add the exception insert
int exceptionStart = ops.mCount;
ops.newException(cv);
// Also add the attendees, because they need to be copied over from the parent event
boolean attendeesRedacted = false;
if (attendeeValues != null) {
for (ContentValues attValues: attendeeValues) {
// If this is the user, use his busy status for attendee status
String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL);
// Note that the exception at which we surpass the redaction limit might have
// any number of attendees shown; since this is an edge case and a workaround,
// it seems to be an acceptable implementation
if (mAccount.mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
attValues.put(Attendees.ATTENDEE_STATUS,
CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
ops.newAttendee(attValues, exceptionStart);
} else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) {
ops.newAttendee(attValues, exceptionStart);
} else {
attendeesRedacted = true;
}
}
}
// And add the parent's reminder value
if (reminderMins > 0) {
ops.newReminder(reminderMins, exceptionStart);
}
if (attendeesRedacted) {
LogUtils.i(TAG, "Attendees redacted in this exception");
}
}
private static int encodeVisibility(int easVisibility) {
int visibility = 0;
switch(easVisibility) {
case 0:
visibility = Events.ACCESS_DEFAULT;
break;
case 1:
visibility = Events.ACCESS_PUBLIC;
break;
case 2:
visibility = Events.ACCESS_PRIVATE;
break;
case 3:
visibility = Events.ACCESS_CONFIDENTIAL;
break;
}
return visibility;
}
private void exceptionsParser(CalendarOperations ops, ContentValues cv,
ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
long startTime, long endTime) throws IOException {
while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
switch (tag) {
case Tags.CALENDAR_EXCEPTION:
exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus,
startTime, endTime);
break;
default:
skipTag();
}
}
}
private String categoriesParser() throws IOException {
StringBuilder categories = new StringBuilder();
while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
switch (tag) {
case Tags.CALENDAR_CATEGORY:
// TODO Handle categories (there's no similar concept for gdata AFAIK)
// We need to save them and spit them back when we update the event
categories.append(getValue());
categories.append(CATEGORY_TOKENIZER_DELIMITER);
break;
default:
skipTag();
}
}
return categories.toString();
}
/**
* For now, we ignore (but still have to parse) event attachments; these are new in EAS 14
*/
private void attachmentsParser() throws IOException {
while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) {
switch (tag) {
case Tags.CALENDAR_ATTACHMENT:
skipParser(Tags.CALENDAR_ATTACHMENT);
break;
default:
skipTag();
}
}
}
private ArrayList<ContentValues> attendeesParser()
throws IOException {
int attendeeCount = 0;
ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
switch (tag) {
case Tags.CALENDAR_ATTENDEE:
ContentValues cv = attendeeParser();
// If we're going to redact these attendees anyway, let's avoid unnecessary
// memory pressure, and not keep them around
// We still need to parse them all, however
attendeeCount++;
// Allow one more than MAX_ATTENDEES, so that the check for "too many" will
// succeed in addEvent
if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) {
attendeeValues.add(cv);
}
break;
default:
skipTag();
}
}
return attendeeValues;
}
private ContentValues attendeeParser()
throws IOException {
ContentValues cv = new ContentValues();
while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
switch (tag) {
case Tags.CALENDAR_ATTENDEE_EMAIL:
cv.put(Attendees.ATTENDEE_EMAIL, getValue());
break;
case Tags.CALENDAR_ATTENDEE_NAME:
cv.put(Attendees.ATTENDEE_NAME, getValue());
break;
case Tags.CALENDAR_ATTENDEE_STATUS:
int status = getValueInt();
cv.put(Attendees.ATTENDEE_STATUS,
(status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
(status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
(status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
(status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
Attendees.ATTENDEE_STATUS_NONE);
break;
case Tags.CALENDAR_ATTENDEE_TYPE:
int type = Attendees.TYPE_NONE;
// EAS types: 1 = req'd, 2 = opt, 3 = resource
switch (getValueInt()) {
case 1:
type = Attendees.TYPE_REQUIRED;
break;
case 2:
type = Attendees.TYPE_OPTIONAL;
break;
}
cv.put(Attendees.ATTENDEE_TYPE, type);
break;
default:
skipTag();
}
}
cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
return cv;
}
private String bodyParser() throws IOException {
String body = null;
while (nextTag(Tags.BASE_BODY) != END) {
switch (tag) {
case Tags.BASE_DATA:
body = getValue();
break;
default:
skipTag();
}
}
// Handle null data without error
if (body == null) return "";
// Remove \r's from any body text
return body.replace("\r\n", "\n");
}
public void addParser(CalendarOperations ops) throws IOException {
String serverId = null;
while (nextTag(Tags.SYNC_ADD) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID: // same as
serverId = getValue();
break;
case Tags.SYNC_APPLICATION_DATA:
addEvent(ops, serverId, false);
break;
default:
skipTag();
}
}
}
private Cursor getServerIdCursor(String serverId) {
return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION,
SERVER_ID_AND_CALENDAR_ID, new String[] {serverId, Long.toString(mCalendarId)},
null);
}
private Cursor getClientIdCursor(String clientId) {
mBindArgument[0] = clientId;
return mContentResolver.query(Events.CONTENT_URI, ID_PROJECTION, CLIENT_ID_SELECTION,
mBindArgument, null);
}
public void deleteParser(CalendarOperations ops) throws IOException {
while (nextTag(Tags.SYNC_DELETE) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
String serverId = getValue();
// Find the event with the given serverId
Cursor c = getServerIdCursor(serverId);
try {
if (c.moveToFirst()) {
userLog("Deleting ", serverId);
ops.delete(c.getLong(0), serverId);
}
} finally {
c.close();
}
break;
default:
skipTag();
}
}
}
/**
* A change is handled as a delete (including all exceptions) and an add
* This isn't as efficient as attempting to traverse the original and all of its exceptions,
* but changes happen infrequently and this code is both simpler and easier to maintain
* @param ops the array of pending ContactProviderOperations.
* @throws IOException
*/
public void changeParser(CalendarOperations ops) throws IOException {
String serverId = null;
while (nextTag(Tags.SYNC_CHANGE) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
serverId = getValue();
break;
case Tags.SYNC_APPLICATION_DATA:
userLog("Changing " + serverId);
addEvent(ops, serverId, true);
break;
default:
skipTag();
}
}
}
@Override
public void commandsParser() throws IOException {
while (nextTag(Tags.SYNC_COMMANDS) != END) {
if (tag == Tags.SYNC_ADD) {
addParser(mOps);
} else if (tag == Tags.SYNC_DELETE) {
deleteParser(mOps);
} else if (tag == Tags.SYNC_CHANGE) {
changeParser(mOps);
} else
skipTag();
}
}
@Override
public void commit() throws IOException {
userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
// Save the syncKey here, using the Helper provider by Calendar provider
mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation(
asSyncAdapter(SyncState.CONTENT_URI, mAccount.mEmailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
mAccountManagerAccount,
mMailbox.mSyncKey.getBytes())));
// Execute our CPO's safely
try {
safeExecute(mContentResolver, CalendarContract.AUTHORITY, mOps);
} catch (RemoteException e) {
throw new IOException("Remote exception caught; will retry");
}
}
public void addResponsesParser() throws IOException {
String serverId = null;
String clientId = null;
int status = -1;
ContentValues cv = new ContentValues();
while (nextTag(Tags.SYNC_ADD) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
serverId = getValue();
break;
case Tags.SYNC_CLIENT_ID:
clientId = getValue();
break;
case Tags.SYNC_STATUS:
status = getValueInt();
if (status != 1) {
userLog("Attempt to add event failed with status: " + status);
}
break;
default:
skipTag();
}
}
if (clientId == null) return;
if (serverId == null) {
// TODO Reconsider how to handle this
serverId = "FAIL:" + status;
}
Cursor c = getClientIdCursor(clientId);
try {
if (c.moveToFirst()) {
cv.put(Events._SYNC_ID, serverId);
cv.put(Events.SYNC_DATA2, clientId);
long id = c.getLong(0);
// Write the serverId into the Event
mOps.add(new Operation(ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id))
.withValues(cv)));
userLog("New event " + clientId + " was given serverId: " + serverId);
}
} finally {
c.close();
}
}
public void changeResponsesParser() throws IOException {
String serverId = null;
String status = null;
while (nextTag(Tags.SYNC_CHANGE) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
serverId = getValue();
break;
case Tags.SYNC_STATUS:
status = getValue();
break;
default:
skipTag();
}
}
if (serverId != null && status != null) {
userLog("Changed event " + serverId + " failed with status: " + status);
}
}
@Override
public void responsesParser() throws IOException {
// Handle server responses here (for Add and Change)
while (nextTag(Tags.SYNC_RESPONSES) != END) {
if (tag == Tags.SYNC_ADD) {
addResponsesParser();
} else if (tag == Tags.SYNC_CHANGE) {
changeResponsesParser();
} else
skipTag();
}
}
/**
* We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties,
* and we just return quickly if the service has already been stopped.
*/
private static ContentProviderResult[] execute(final ContentResolver contentResolver,
final String authority, final ArrayList<ContentProviderOperation> ops)
throws RemoteException, OperationApplicationException {
if (!ops.isEmpty()) {
ContentProviderResult[] result = contentResolver.applyBatch(authority, ops);
//mService.userLog("Results: " + result.length);
return result;
}
return new ContentProviderResult[0];
}
/**
* Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
* passed-in offset
*/
@VisibleForTesting
static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
if (op.mOp != null) {
return op.mOp;
} else if (op.mBuilder == null) {
throw new IllegalArgumentException("Operation must have CPO.Builder");
}
ContentProviderOperation.Builder builder = op.mBuilder;
if (op.mColumnName != null) {
builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
}
return builder.build();
}
/**
* Create a list of CPOs from a list of Operations, and then apply them in a batch
*/
private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver,
final String authority, final ArrayList<Operation> ops, final int offset)
throws RemoteException, OperationApplicationException {
// Handle the empty case
if (ops.isEmpty()) {
return new ContentProviderResult[0];
}
ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
for (Operation op: ops) {
cpos.add(operationToContentProviderOperation(op, offset));
}
return execute(contentResolver, authority, cpos);
}
/**
* Apply the list of CPO's in the provider and copy the "mini" result into our full result array
*/
private static void applyAndCopyResults(final ContentResolver contentResolver,
final String authority, final ArrayList<Operation> mini,
final ContentProviderResult[] result, final int offset) throws RemoteException {
// Empty lists are ok; we just ignore them
if (mini.isEmpty()) return;
try {
ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini,
offset);
// Copy the results from this mini-batch into our results array
System.arraycopy(miniResult, 0, result, offset, miniResult.length);
} catch (OperationApplicationException e) {
// Not possible since we're building the ops ourselves
}
}
/**
* Called by a sync adapter to execute a list of Operations in the ContentProvider handling
* the passed-in authority. If the attempt to apply the batch fails due to a too-large
* binder transaction, we split the Operations as directed by separators. If any of the
* "mini" batches fails due to a too-large transaction, we're screwed, but this would be
* vanishingly rare. Other, possibly transient, errors are handled by throwing a
* RemoteException, which the caller will likely re-throw as an IOException so that the sync
* can be attempted again.
*
* Callers MAY leave a dangling separator at the end of the list; note that the separators
* themselves are only markers and are not sent to the provider.
*/
protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver,
final String authority, final ArrayList<Operation> ops) throws RemoteException {
//mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority);
ContentProviderResult[] result = null;
try {
// Try to execute the whole thing
return applyBatch(contentResolver, authority, ops, 0);
} catch (TransactionTooLargeException e) {
// Nope; split into smaller chunks, demarcated by the separator operation
//mService.userLog("Transaction too large; spliting!");
ArrayList<Operation> mini = new ArrayList<Operation>();
// Build a result array with the total size we're sending
result = new ContentProviderResult[ops.size()];
int count = 0;
int offset = 0;
for (Operation op: ops) {
if (op.mSeparator) {
try {
//mService.userLog("Try mini-batch of ", mini.size(), " CPO's");
applyAndCopyResults(contentResolver, authority, mini, result, offset);
mini.clear();
// Save away the offset here; this will need to be subtracted out of the
// value originally set by the adapter
offset = count + 1; // Remember to add 1 for the separator!
} catch (TransactionTooLargeException e1) {
throw new RuntimeException("Can't send transaction; sync stopped.");
} catch (RemoteException e1) {
throw e1;
}
} else {
mini.add(op);
}
count++;
}
// Check out what's left; if it's more than just a separator, apply the batch
int miniSize = mini.size();
if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
applyAndCopyResults(contentResolver, authority, mini, result, offset);
}
} catch (RemoteException e) {
throw e;
} catch (OperationApplicationException e) {
// Not possible since we're building the ops ourselves
}
return result;
}
/**
* Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
*/
protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
Operation op = new Operation(
ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
op.mSeparator = true;
ops.add(op);
}
}