Fix upsync for calendars.
Change-Id: If3d48a942c145b6e3e2c08c17264a54b1805de16
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 236a063..2fab97b 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -1220,64 +1220,12 @@
mAccountManagerAccount,
mMailbox.mSyncKey.getBytes())));
- // We need to send cancellations now, because the Event won't exist after the commit
- // TODO: Fix Upsync. (These lists are set by upsync.)
- /*
- for (long eventId: mSendCancelIdList) {
- EmailContent.Message msg;
- try {
- msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
- EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL, null,
- mAccount);
- } catch (RemoteException e) {
- // Nothing to do here; the Event may no longer exist
- continue;
- }
- if (msg != null) {
- EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
- }
- }
- */
-
// Execute our CPO's safely
try {
mOps.mResults = safeExecute(mContentResolver, CalendarContract.AUTHORITY, mOps);
} catch (RemoteException e) {
throw new IOException("Remote exception caught; will retry");
}
-
- if (mOps.mResults != null) {
- // TODO: Fix Upsync. (These lists are set by upsync.)
- /*
- // Clear dirty and mark flags for updates sent to server
- if (!mUploadedIdList.isEmpty()) {
- ContentValues cv = new ContentValues();
- cv.put(Events.DIRTY, 0);
- cv.put(EVENT_SYNC_MARK, "0");
- for (long eventId : mUploadedIdList) {
- mContentResolver.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
- null, null);
- }
- }
- // Delete events marked for deletion
- if (!mDeletedIdList.isEmpty()) {
- for (long eventId : mDeletedIdList) {
- mContentResolver.delete(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- null, null);
- }
- }
- // Send any queued up email (invitations replies, etc.)
- for (Message msg: mOutgoingMailList) {
- EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
- }
- */
- }
}
public void addResponsesParser() throws IOException {
@@ -1798,385 +1746,381 @@
return false;
}
+ // We've got to handle exceptions as part of the parent when changes occur, so we need
+ // to find new/changed exceptions and mark the parent dirty
+ ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
+ Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION,
+ DIRTY_EXCEPTION_IN_CALENDAR, mCalendarIdArgument, null);
try {
- // We've got to handle exceptions as part of the parent when changes occur, so we need
- // to find new/changed exceptions and mark the parent dirty
- ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
- Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION,
- DIRTY_EXCEPTION_IN_CALENDAR, mCalendarIdArgument, null);
- try {
- ContentValues cv = new ContentValues();
- // We use _sync_mark here to distinguish dirty parents from parents with dirty
- // exceptions
- cv.put(EVENT_SYNC_MARK, "1");
- while (c.moveToNext()) {
- // Mark the parents of dirty exceptions
- long parentId = c.getLong(0);
- int cnt = cr.update(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
- EVENT_ID_AND_CALENDAR_ID, new String[] {
- Long.toString(parentId), mCalendarIdString
- });
- // Keep track of any orphaned exceptions
- if (cnt == 0) {
- orphanedExceptions.add(c.getLong(1));
+ ContentValues cv = new ContentValues();
+ // We use _sync_mark here to distinguish dirty parents from parents with dirty
+ // exceptions
+ cv.put(EVENT_SYNC_MARK, "1");
+ while (c.moveToNext()) {
+ // Mark the parents of dirty exceptions
+ long parentId = c.getLong(0);
+ int cnt = cr.update(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
+ EVENT_ID_AND_CALENDAR_ID, new String[] {
+ Long.toString(parentId), mCalendarIdString
+ });
+ // Keep track of any orphaned exceptions
+ if (cnt == 0) {
+ orphanedExceptions.add(c.getLong(1));
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Delete any orphaned exceptions
+ for (long orphan : orphanedExceptions) {
+ userLog(TAG, "Deleted orphaned exception: " + orphan);
+ cr.delete(
+ asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, orphan),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null, null);
+ }
+ orphanedExceptions.clear();
+
+ // Now we can go through dirty/marked top-level events and send them
+ // back to the server
+ EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
+ DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, mCalendarIdArgument, null), cr);
+ ContentValues cidValues = new ContentValues();
+
+ try {
+ boolean first = true;
+ while (eventIterator.hasNext()) {
+ Entity entity = eventIterator.next();
+
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(Events._SYNC_ID);
+
+ // We first need to check whether we can upsync this event; our test for this
+ // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
+ // If this is set to "1", we can't upsync the event
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ ContentValues ncvValues = ncv.values;
+ if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
+ EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
+ if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
+ // Make sure we mark this to clear the dirty flag
+ mUploadedIdList.add(entityValues.getAsLong(Events._ID));
+ continue;
+ }
+ }
}
}
- } finally {
- c.close();
- }
- // Delete any orphaned exceptions
- for (long orphan : orphanedExceptions) {
- userLog(TAG, "Deleted orphaned exception: " + orphan);
- cr.delete(
- asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, orphan),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null, null);
- }
- orphanedExceptions.clear();
+ // Find our uid in the entity; otherwise create one
+ String clientId = entityValues.getAsString(Events.SYNC_DATA2);
+ if (clientId == null) {
+ clientId = UUID.randomUUID().toString();
+ }
- // Now we can go through dirty/marked top-level events and send them
- // back to the server
- EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
- DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, mCalendarIdArgument, null), cr);
- ContentValues cidValues = new ContentValues();
+ // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+ // We can generate all but what we're testing for below
+ String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
+ boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
- try {
- boolean first = true;
- while (eventIterator.hasNext()) {
- Entity entity = eventIterator.next();
+ if (!entityValues.containsKey(Events.DTSTART)
+ || (!entityValues.containsKey(Events.DURATION) &&
+ !entityValues.containsKey(Events.DTEND))
+ || organizerEmail == null) {
+ continue;
+ }
- // For each of these entities, create the change commands
- ContentValues entityValues = entity.getEntityValues();
- String serverId = entityValues.getAsString(Events._SYNC_ID);
-
- // We first need to check whether we can upsync this event; our test for this
- // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
- // If this is set to "1", we can't upsync the event
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
- ContentValues ncvValues = ncv.values;
- if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
- EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
- if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
- // Make sure we mark this to clear the dirty flag
- mUploadedIdList.add(entityValues.getAsLong(Events._ID));
- continue;
- }
- }
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Calendar changes to the server");
+ first = false;
+ }
+ long eventId = entityValues.getAsLong(Events._ID);
+ if (serverId == null) {
+ // This is a new event; create a clientId
+ userLog("Creating new event with clientId: ", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the Event as the local id
+ cidValues.put(Events.SYNC_DATA2, clientId);
+ cidValues.put(EVENT_SYNC_VERSION, "0");
+ cr.update(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cidValues, null, null);
+ } else {
+ if (entityValues.getAsInteger(Events.DELETED) == 1) {
+ userLog("Deleting event with serverId: ", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(eventId);
+ if (selfOrganizer) {
+ mSendCancelIdList.add(eventId);
+ } else {
+ sendDeclinedEmail(entity, clientId);
}
- }
-
- // Find our uid in the entity; otherwise create one
- String clientId = entityValues.getAsString(Events.SYNC_DATA2);
- if (clientId == null) {
- clientId = UUID.randomUUID().toString();
- }
-
- // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
- // We can generate all but what we're testing for below
- String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
- boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
-
- if (!entityValues.containsKey(Events.DTSTART)
- || (!entityValues.containsKey(Events.DURATION) &&
- !entityValues.containsKey(Events.DTEND))
- || organizerEmail == null) {
continue;
}
-
- if (first) {
- s.start(Tags.SYNC_COMMANDS);
- userLog("Sending Calendar changes to the server");
- first = false;
- }
- long eventId = entityValues.getAsLong(Events._ID);
- if (serverId == null) {
- // This is a new event; create a clientId
- userLog("Creating new event with clientId: ", clientId);
- s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
- // And save it in the Event as the local id
- cidValues.put(Events.SYNC_DATA2, clientId);
- cidValues.put(EVENT_SYNC_VERSION, "0");
- cr.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
+ userLog("Upsync change to event with serverId: " + serverId);
+ // Get the current version
+ String version = entityValues.getAsString(EVENT_SYNC_VERSION);
+ // This should never be null, but catch this error anyway
+ // Version should be "0" when we create the event, so use that
+ if (version == null) {
+ version = "0";
} else {
- if (entityValues.getAsInteger(Events.DELETED) == 1) {
- userLog("Deleting event with serverId: ", serverId);
- s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
- mDeletedIdList.add(eventId);
- if (selfOrganizer) {
- mSendCancelIdList.add(eventId);
- } else {
- sendDeclinedEmail(entity, clientId);
- }
- continue;
- }
- userLog("Upsync change to event with serverId: " + serverId);
- // Get the current version
- String version = entityValues.getAsString(EVENT_SYNC_VERSION);
- // This should never be null, but catch this error anyway
- // Version should be "0" when we create the event, so use that
- if (version == null) {
+ // Increment and save
+ try {
+ version = Integer.toString((Integer.parseInt(version) + 1));
+ } catch (Exception e) {
+ // Handle the case in which someone writes a non-integer here;
+ // shouldn't happen, but we don't want to kill the sync for his
version = "0";
- } else {
- // Increment and save
- try {
- version = Integer.toString((Integer.parseInt(version) + 1));
- } catch (Exception e) {
- // Handle the case in which someone writes a non-integer here;
- // shouldn't happen, but we don't want to kill the sync for his
- version = "0";
- }
- }
- cidValues.put(EVENT_SYNC_VERSION, version);
- // Also save in entityValues so that we send it this time around
- entityValues.put(EVENT_SYNC_VERSION, version);
- cr.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
- s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
- }
- s.start(Tags.SYNC_APPLICATION_DATA);
-
- sendEvent(entity, clientId, s);
-
- // Now, the hard part; find exceptions for this event
- if (serverId != null) {
- EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
- ORIGINAL_EVENT_AND_CALENDAR, new String[] {
- serverId, mCalendarIdString
- }, null), cr);
- boolean exFirst = true;
- while (exIterator.hasNext()) {
- Entity exEntity = exIterator.next();
- if (exFirst) {
- s.start(Tags.CALENDAR_EXCEPTIONS);
- exFirst = false;
- }
- s.start(Tags.CALENDAR_EXCEPTION);
- sendEvent(exEntity, null, s);
- ContentValues exValues = exEntity.getEntityValues();
- if (getInt(exValues, Events.DIRTY) == 1) {
- // This is a new/updated exception, so we've got to notify our
- // attendees about it
- long exEventId = exValues.getAsLong(Events._ID);
- int flag;
-
- // Copy subvalues into the exception; otherwise, we won't see the
- // attendees when preparing the message
- for (NamedContentValues ncv: entity.getSubValues()) {
- exEntity.addSubValue(ncv.uri, ncv.values);
- }
-
- if ((getInt(exValues, Events.DELETED) == 1) ||
- (getInt(exValues, Events.STATUS) ==
- Events.STATUS_CANCELED)) {
- flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
- if (!selfOrganizer) {
- // Send a cancellation notice to the organizer
- // Since CalendarProvider2 sets the organizer of exceptions
- // to the user, we have to reset it first to the original
- // organizer
- exValues.put(Events.ORGANIZER,
- entityValues.getAsString(Events.ORGANIZER));
- sendDeclinedEmail(exEntity, clientId);
- }
- } else {
- flag = Message.FLAG_OUTGOING_MEETING_INVITE;
- }
- // Add the eventId of the exception to the uploaded id list, so that
- // the dirty/mark bits are cleared
- mUploadedIdList.add(exEventId);
-
- // Copy version so the ics attachment shows the proper sequence #
- exValues.put(EVENT_SYNC_VERSION,
- entityValues.getAsString(EVENT_SYNC_VERSION));
- // Copy location so that it's included in the outgoing email
- if (entityValues.containsKey(Events.EVENT_LOCATION)) {
- exValues.put(Events.EVENT_LOCATION,
- entityValues.getAsString(Events.EVENT_LOCATION));
- }
-
- if (selfOrganizer) {
- Message msg =
- CalendarUtilities.createMessageForEntity(mContext,
- exEntity, flag, clientId, mAccount);
- if (msg != null) {
- userLog("Queueing exception update to " + msg.mTo);
- mOutgoingMailList.add(msg);
- }
- }
- }
- s.end(); // EXCEPTION
- }
- if (!exFirst) {
- s.end(); // EXCEPTIONS
}
}
-
- s.end().end(); // ApplicationData & Change
- mUploadedIdList.add(eventId);
-
- // Go through the extended properties of this Event and pull out our tokenized
- // attendees list and the user attendee status; we will need them later
- String attendeeString = null;
- long attendeeStringId = -1;
- String userAttendeeStatus = null;
- long userAttendeeStatusId = -1;
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
- ContentValues ncvValues = ncv.values;
- String propertyName =
- ncvValues.getAsString(ExtendedProperties.NAME);
- if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
- attendeeString =
- ncvValues.getAsString(ExtendedProperties.VALUE);
- attendeeStringId =
- ncvValues.getAsLong(ExtendedProperties._ID);
- } else if (propertyName.equals(
- EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
- userAttendeeStatus =
- ncvValues.getAsString(ExtendedProperties.VALUE);
- userAttendeeStatusId =
- ncvValues.getAsLong(ExtendedProperties._ID);
- }
- }
- }
-
- // Send the meeting invite if there are attendees and we're the organizer AND
- // if the Event itself is dirty (we might be syncing only because an exception
- // is dirty, in which case we DON'T send email about the Event)
- if (selfOrganizer &&
- (getInt(entityValues, Events.DIRTY) == 1)) {
- EmailContent.Message msg =
- CalendarUtilities.createMessageForEventId(mContext, eventId,
- EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE, clientId,
- mAccount);
- if (msg != null) {
- userLog("Queueing invitation to ", msg.mTo);
- mOutgoingMailList.add(msg);
- }
- // Make a list out of our tokenized attendees, if we have any
- ArrayList<String> originalAttendeeList = new ArrayList<String>();
- if (attendeeString != null) {
- StringTokenizer st =
- new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
- while (st.hasMoreTokens()) {
- originalAttendeeList.add(st.nextToken());
- }
- }
- StringBuilder newTokenizedAttendees = new StringBuilder();
- // See if any attendees have been dropped and while we're at it, build
- // an updated String with tokenized attendee addresses
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(Attendees.CONTENT_URI)) {
- String attendeeEmail =
- ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
- // Remove all found attendees
- originalAttendeeList.remove(attendeeEmail);
- newTokenizedAttendees.append(attendeeEmail);
- newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
- }
- }
- // Update extended properties with the new attendee list, if we have one
- // Otherwise, create one (this would be the case for Events created on
- // device or "legacy" events (before this code was added)
- ContentValues cv = new ContentValues();
- cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
- if (attendeeString != null) {
- cr.update(asSyncAdapter(ContentUris.withAppendedId(
- ExtendedProperties.CONTENT_URI, attendeeStringId),
+ cidValues.put(EVENT_SYNC_VERSION, version);
+ // Also save in entityValues so that we send it this time around
+ entityValues.put(EVENT_SYNC_VERSION, version);
+ cr.update(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cv, null, null);
- } else {
- // If there wasn't an "attendees" property, insert one
- cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
- cv.put(ExtendedProperties.EVENT_ID, eventId);
- cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI,
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
+ cidValues, null, null);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+
+ sendEvent(entity, clientId, s);
+
+ // Now, the hard part; find exceptions for this event
+ if (serverId != null) {
+ EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
+ ORIGINAL_EVENT_AND_CALENDAR, new String[] {
+ serverId, mCalendarIdString
+ }, null), cr);
+ boolean exFirst = true;
+ while (exIterator.hasNext()) {
+ Entity exEntity = exIterator.next();
+ if (exFirst) {
+ s.start(Tags.CALENDAR_EXCEPTIONS);
+ exFirst = false;
}
- // Whoever is left has been removed from the attendee list; send them
- // a cancellation
- for (String removedAttendee: originalAttendeeList) {
- // Send a cancellation message to each of them
- msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
- Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
- removedAttendee);
- if (msg != null) {
- // Just send it to the removed attendee
- userLog("Queueing cancellation to removed attendee " + msg.mTo);
- mOutgoingMailList.add(msg);
+ s.start(Tags.CALENDAR_EXCEPTION);
+ sendEvent(exEntity, null, s);
+ ContentValues exValues = exEntity.getEntityValues();
+ if (getInt(exValues, Events.DIRTY) == 1) {
+ // This is a new/updated exception, so we've got to notify our
+ // attendees about it
+ long exEventId = exValues.getAsLong(Events._ID);
+ int flag;
+
+ // Copy subvalues into the exception; otherwise, we won't see the
+ // attendees when preparing the message
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ exEntity.addSubValue(ncv.uri, ncv.values);
}
- }
- } else if (!selfOrganizer) {
- // If we're not the organizer, see if we've changed our attendee status
- // Our last synced attendee status is in ExtendedProperties, and we've
- // retrieved it above as userAttendeeStatus
- int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
- int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
- if (userAttendeeStatus != null) {
- try {
- syncStatus = Integer.parseInt(userAttendeeStatus);
- } catch (NumberFormatException e) {
- // Just in case somebody else mucked with this and it's not Integer
+
+ if ((getInt(exValues, Events.DELETED) == 1) ||
+ (getInt(exValues, Events.STATUS) ==
+ Events.STATUS_CANCELED)) {
+ flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
+ if (!selfOrganizer) {
+ // Send a cancellation notice to the organizer
+ // Since CalendarProvider2 sets the organizer of exceptions
+ // to the user, we have to reset it first to the original
+ // organizer
+ exValues.put(Events.ORGANIZER,
+ entityValues.getAsString(Events.ORGANIZER));
+ sendDeclinedEmail(exEntity, clientId);
+ }
+ } else {
+ flag = Message.FLAG_OUTGOING_MEETING_INVITE;
}
- }
- if ((currentStatus != syncStatus) &&
- (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
- // If so, send a meeting reply
- int messageFlag = 0;
- switch (currentStatus) {
- case Attendees.ATTENDEE_STATUS_ACCEPTED:
- messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
- break;
- case Attendees.ATTENDEE_STATUS_DECLINED:
- messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
- break;
- case Attendees.ATTENDEE_STATUS_TENTATIVE:
- messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
- break;
+ // Add the eventId of the exception to the uploaded id list, so that
+ // the dirty/mark bits are cleared
+ mUploadedIdList.add(exEventId);
+
+ // Copy version so the ics attachment shows the proper sequence #
+ exValues.put(EVENT_SYNC_VERSION,
+ entityValues.getAsString(EVENT_SYNC_VERSION));
+ // Copy location so that it's included in the outgoing email
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ exValues.put(Events.EVENT_LOCATION,
+ entityValues.getAsString(Events.EVENT_LOCATION));
}
- // Make sure we have a valid status (messageFlag should never be zero)
- if (messageFlag != 0 && userAttendeeStatusId >= 0) {
- // Save away the new status
- cidValues.clear();
- cidValues.put(ExtendedProperties.VALUE,
- Integer.toString(currentStatus));
- cr.update(asSyncAdapter(ContentUris.withAppendedId(
- ExtendedProperties.CONTENT_URI, userAttendeeStatusId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
- // Send mail to the organizer advising of the new status
- EmailContent.Message msg =
- CalendarUtilities.createMessageForEventId(mContext, eventId,
- messageFlag, clientId, mAccount);
+
+ if (selfOrganizer) {
+ Message msg =
+ CalendarUtilities.createMessageForEntity(mContext,
+ exEntity, flag, clientId, mAccount);
if (msg != null) {
- userLog("Queueing invitation reply to " + msg.mTo);
+ userLog("Queueing exception update to " + msg.mTo);
mOutgoingMailList.add(msg);
}
}
}
+ s.end(); // EXCEPTION
+ }
+ if (!exFirst) {
+ s.end(); // EXCEPTIONS
}
}
- if (!first) {
- s.end(); // Commands
+
+ s.end().end(); // ApplicationData & Change
+ mUploadedIdList.add(eventId);
+
+ // Go through the extended properties of this Event and pull out our tokenized
+ // attendees list and the user attendee status; we will need them later
+ String attendeeString = null;
+ long attendeeStringId = -1;
+ String userAttendeeStatus = null;
+ long userAttendeeStatusId = -1;
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ ContentValues ncvValues = ncv.values;
+ String propertyName =
+ ncvValues.getAsString(ExtendedProperties.NAME);
+ if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
+ attendeeString =
+ ncvValues.getAsString(ExtendedProperties.VALUE);
+ attendeeStringId =
+ ncvValues.getAsLong(ExtendedProperties._ID);
+ } else if (propertyName.equals(
+ EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
+ userAttendeeStatus =
+ ncvValues.getAsString(ExtendedProperties.VALUE);
+ userAttendeeStatusId =
+ ncvValues.getAsLong(ExtendedProperties._ID);
+ }
+ }
}
- } finally {
- eventIterator.close();
+
+ // Send the meeting invite if there are attendees and we're the organizer AND
+ // if the Event itself is dirty (we might be syncing only because an exception
+ // is dirty, in which case we DON'T send email about the Event)
+ if (selfOrganizer &&
+ (getInt(entityValues, Events.DIRTY) == 1)) {
+ EmailContent.Message msg =
+ CalendarUtilities.createMessageForEventId(mContext, eventId,
+ EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE, clientId,
+ mAccount);
+ if (msg != null) {
+ userLog("Queueing invitation to ", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ // Make a list out of our tokenized attendees, if we have any
+ ArrayList<String> originalAttendeeList = new ArrayList<String>();
+ if (attendeeString != null) {
+ StringTokenizer st =
+ new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
+ while (st.hasMoreTokens()) {
+ originalAttendeeList.add(st.nextToken());
+ }
+ }
+ StringBuilder newTokenizedAttendees = new StringBuilder();
+ // See if any attendees have been dropped and while we're at it, build
+ // an updated String with tokenized attendee addresses
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(Attendees.CONTENT_URI)) {
+ String attendeeEmail =
+ ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
+ // Remove all found attendees
+ originalAttendeeList.remove(attendeeEmail);
+ newTokenizedAttendees.append(attendeeEmail);
+ newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
+ }
+ }
+ // Update extended properties with the new attendee list, if we have one
+ // Otherwise, create one (this would be the case for Events created on
+ // device or "legacy" events (before this code was added)
+ ContentValues cv = new ContentValues();
+ cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
+ if (attendeeString != null) {
+ cr.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, attendeeStringId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cv, null, null);
+ } else {
+ // If there wasn't an "attendees" property, insert one
+ cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
+ cv.put(ExtendedProperties.EVENT_ID, eventId);
+ cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI,
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
+ }
+ // Whoever is left has been removed from the attendee list; send them
+ // a cancellation
+ for (String removedAttendee: originalAttendeeList) {
+ // Send a cancellation message to each of them
+ msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
+ Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
+ removedAttendee);
+ if (msg != null) {
+ // Just send it to the removed attendee
+ userLog("Queueing cancellation to removed attendee " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ } else if (!selfOrganizer) {
+ // If we're not the organizer, see if we've changed our attendee status
+ // Our last synced attendee status is in ExtendedProperties, and we've
+ // retrieved it above as userAttendeeStatus
+ int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
+ int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
+ if (userAttendeeStatus != null) {
+ try {
+ syncStatus = Integer.parseInt(userAttendeeStatus);
+ } catch (NumberFormatException e) {
+ // Just in case somebody else mucked with this and it's not Integer
+ }
+ }
+ if ((currentStatus != syncStatus) &&
+ (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
+ // If so, send a meeting reply
+ int messageFlag = 0;
+ switch (currentStatus) {
+ case Attendees.ATTENDEE_STATUS_ACCEPTED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
+ break;
+ case Attendees.ATTENDEE_STATUS_DECLINED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
+ break;
+ case Attendees.ATTENDEE_STATUS_TENTATIVE:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
+ break;
+ }
+ // Make sure we have a valid status (messageFlag should never be zero)
+ if (messageFlag != 0 && userAttendeeStatusId >= 0) {
+ // Save away the new status
+ cidValues.clear();
+ cidValues.put(ExtendedProperties.VALUE,
+ Integer.toString(currentStatus));
+ cr.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, userAttendeeStatusId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cidValues, null, null);
+ // Send mail to the organizer advising of the new status
+ EmailContent.Message msg =
+ CalendarUtilities.createMessageForEventId(mContext, eventId,
+ messageFlag, clientId, mAccount);
+ if (msg != null) {
+ userLog("Queueing invitation reply to " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ }
+ }
}
- } catch (RemoteException e) {
- LogUtils.e(TAG, "Could not read dirty events.");
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ eventIterator.close();
}
return false;
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
index 960be47..2372459 100644
--- a/src/com/android/exchange/adapter/Serializer.java
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -223,7 +223,7 @@
out.write(0);
}
- void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
+ public void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
String value = cv.getAsString(key);
if (value != null && value.length() > 0) {
data(tag, value);
diff --git a/src/com/android/exchange/service/CalendarSyncAdapterService.java b/src/com/android/exchange/service/CalendarSyncAdapterService.java
index 76f3bd6..4fb2d5a 100644
--- a/src/com/android/exchange/service/CalendarSyncAdapterService.java
+++ b/src/com/android/exchange/service/CalendarSyncAdapterService.java
@@ -34,7 +34,7 @@
import com.android.mail.utils.LogUtils;
public class CalendarSyncAdapterService extends AbstractSyncAdapterService {
- private static final String TAG = "EAS CalendarSyncAdapterService";
+ private static final String TAG = "EASCalSyncAdaptSvc";
private static final String ACCOUNT_AND_TYPE_CALENDAR =
MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
private static final String DIRTY_IN_ACCOUNT =
diff --git a/src/com/android/exchange/service/ContactsSyncAdapterService.java b/src/com/android/exchange/service/ContactsSyncAdapterService.java
index 37348c8..32e01a6 100644
--- a/src/com/android/exchange/service/ContactsSyncAdapterService.java
+++ b/src/com/android/exchange/service/ContactsSyncAdapterService.java
@@ -16,13 +16,6 @@
package com.android.exchange.service;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.exchange.Eas;
-import com.android.mail.utils.LogUtils;
-
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
@@ -35,8 +28,15 @@
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.Eas;
+import com.android.mail.utils.LogUtils;
+
public class ContactsSyncAdapterService extends AbstractSyncAdapterService {
- private static final String TAG = "EAS ContactsSyncAdapterService";
+ private static final String TAG = "EASContactsSyncAdaptSvc";
private static final String ACCOUNT_AND_TYPE_CONTACTS =
MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
diff --git a/src/com/android/exchange/service/EasAccountValidator.java b/src/com/android/exchange/service/EasAccountValidator.java
index ae246e1..ea12599 100644
--- a/src/com/android/exchange/service/EasAccountValidator.java
+++ b/src/com/android/exchange/service/EasAccountValidator.java
@@ -1,7 +1,5 @@
package com.android.exchange.service;
-import com.google.common.collect.Sets;
-
import android.content.ContentValues;
import android.content.Context;
import android.os.Build;
@@ -11,6 +9,7 @@
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.PolicyServiceProxy;
@@ -24,6 +23,7 @@
import com.android.exchange.adapter.SettingsParser;
import com.android.exchange.adapter.Tags;
import com.android.mail.utils.LogUtils;
+import com.google.common.collect.Sets;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
@@ -611,6 +611,13 @@
if (bundle != null) {
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
+ } else if (resultCode == MessagingException.NO_ERROR) {
+ // This is an actual sync which succeeded. Let's force the outbox to exist as well.
+ if (Mailbox.findMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX) ==
+ Mailbox.NO_MAILBOX) {
+ Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX)
+ .save(mContext);
+ }
}
}
diff --git a/src/com/android/exchange/service/EasCalendarSyncHandler.java b/src/com/android/exchange/service/EasCalendarSyncHandler.java
index 44c262f..a483fe8 100644
--- a/src/com/android/exchange/service/EasCalendarSyncHandler.java
+++ b/src/com/android/exchange/service/EasCalendarSyncHandler.java
@@ -2,53 +2,133 @@
import android.content.ContentProviderClient;
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.content.SyncResult;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.provider.CalendarContract;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.EventsEntity;
+import android.provider.CalendarContract.ExtendedProperties;
+import android.provider.CalendarContract.Reminders;
+import android.provider.CalendarContract.SyncState;
import android.provider.SyncStateContract;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import com.android.calendarcommon2.DateException;
+import com.android.calendarcommon2.Duration;
import com.android.emailcommon.TrafficFlags;
import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.utility.Utility;
import com.android.exchange.Eas;
import com.android.exchange.adapter.AbstractSyncParser;
import com.android.exchange.adapter.CalendarSyncAdapter;
import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
import com.android.exchange.utility.CalendarUtilities;
+import com.android.mail.utils.LogUtils;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.UUID;
/**
* Performs an Exchange Sync for a Calendar collection.
*/
public class EasCalendarSyncHandler extends EasSyncHandler {
+ private static final String TAG = "EasCalendarSyncHandler";
- private static final String CALENDAR_SELECTION = CalendarContract.Calendars.ACCOUNT_NAME +
- "=? AND " + CalendarContract.Calendars.ACCOUNT_TYPE + "=?";
- private static final int CALENDAR_SELECTION_ID = 0;
+ // TODO: Some constants are copied from CalendarSyncAdapter and are still used by the parser.
+ // These values need to stay in sync; when the parser is cleaned up, be sure to unify them.
+
+ /** Projection for getting a calendar id. */
+ private static final String[] CALENDAR_ID_PROJECTION = { Calendars._ID };
+ private static final int CALENDAR_ID_COLUMN = 0;
+
+ /** Content selection for getting a calendar id for an account. */
+ private static final String CALENDAR_SELECTION = Calendars.ACCOUNT_NAME + "=? AND " +
+ Calendars.ACCOUNT_TYPE + "=?";
+
+ /** The column used to track the timezone of the event. */
+ private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
+
+ /** Used to keep track of exception vs. parent event dirtiness. */
+ private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8;
+
+ /** The column used to track the Event version sequence number. */
+ private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4;
+
+ /** Projection for getting info about changed events. */
+ private static final String[] ORIGINAL_EVENT_PROJECTION = { Events.ORIGINAL_ID, Events._ID };
+ private static final int ORIGINAL_EVENT_ORIGINAL_ID_COLUMN = 0;
+ private static final int ORIGINAL_EVENT_ID_COLUMN = 1;
+
+ /** Content selection for dirty calendar events. */
+ private static final String DIRTY_EXCEPTION_IN_CALENDAR = Events.DIRTY + "=1 AND " +
+ Events.ORIGINAL_ID + " NOTNULL AND " + Events.CALENDAR_ID + "=?";
+
+ /** Where clause for updating dirty events. */
+ private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " +
+ Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
+
+ /** Content selection for dirty or marked top level events. */
+ private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY +
+ "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " + Events.ORIGINAL_ID + " ISNULL AND " +
+ Events.CALENDAR_ID + "=?";
+
+ /** Content selection for getting events when handling exceptions. */
+ private static final String ORIGINAL_EVENT_AND_CALENDAR = Events.ORIGINAL_SYNC_ID + "=? AND " +
+ Events.CALENDAR_ID + "=?";
+
+ private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
+ private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
+
+ /** 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 String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
+ private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
+ private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
private final android.accounts.Account mAccountManagerAccount;
private final long mCalendarId;
+ // The following lists are populated as part of upsync, and handled during cleanup.
+ /** Ids of events that were deleted in this upsync. */
+ private final ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ /** Ids of events that were changed in this upsync. */
+ private final ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
+ /** Emails that need to be sent due to this upsync. */
+ private final ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
+
public EasCalendarSyncHandler(final Context context, final ContentResolver contentResolver,
final android.accounts.Account accountManagerAccount, final Account account,
final Mailbox mailbox, final Bundle syncExtras, final SyncResult syncResult) {
super(context, contentResolver, account, mailbox, syncExtras, syncResult);
mAccountManagerAccount = accountManagerAccount;
- final Cursor c = mContentResolver.query(CalendarContract.Calendars.CONTENT_URI,
- new String[] {CalendarContract.Calendars._ID}, CALENDAR_SELECTION,
+ final Cursor c = mContentResolver.query(Calendars.CONTENT_URI, CALENDAR_ID_PROJECTION,
+ CALENDAR_SELECTION,
new String[] {mAccount.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
if (c == null) {
mCalendarId = -1;
} else {
try {
if (c.moveToFirst()) {
- mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
+ mCalendarId = c.getLong(CALENDAR_ID_COLUMN);
} else {
mCalendarId = CalendarUtilities.createCalendar(mContext, mContentResolver,
mAccount, mMailbox);
@@ -64,12 +144,16 @@
return TrafficFlags.DATA_CALENDAR;
}
+ /**
+ * Adds params to a {@link Uri} to indicate that the caller is a sync adapter, and to add the
+ * account info.
+ * @param uri The {@link Uri} to which to add params.
+ * @return
+ */
private Uri asSyncAdapter(final Uri uri) {
return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
- .appendQueryParameter(
- CalendarContract.Calendars.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(
- CalendarContract.Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(Calendars.ACCOUNT_NAME, mAccount.mEmailAddress)
+ .appendQueryParameter(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.build();
}
@@ -78,11 +162,11 @@
// mMailbox.mSyncKey is bogus since state is stored by the calendar provider, so we
// need to fetch the data from there.
// However, we need for that value to be reasonable, so we set it here once we fetch it.
- final ContentProviderClient client = mContentResolver.acquireContentProviderClient(
- CalendarContract.CONTENT_URI);
+ final ContentProviderClient client =
+ mContentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI);
try {
final byte[] data = SyncStateContract.Helpers.get(client,
- asSyncAdapter(CalendarContract.SyncState.CONTENT_URI), mAccountManagerAccount);
+ asSyncAdapter(SyncState.CONTENT_URI), mAccountManagerAccount);
if (data == null || data.length == 0) {
mMailbox.mSyncKey = "0";
} else {
@@ -116,13 +200,783 @@
setPimSyncOptions(s, Eas.FILTER_2_WEEKS);
}
+ /**
+ * Find all dirty events for our calendar and mark their parents. Also delete any dirty events
+ * that have no parents.
+ * @param calendarIdString {@link #mCalendarId}, as a String.
+ * @param calendarIdArgument calendarIdString, in a String array.
+ */
+ private void markParentsOfDirtyEvents(final String calendarIdString,
+ final String[] calendarIdArgument) {
+ // We've got to handle exceptions as part of the parent when changes occur, so we need
+ // to find new/changed exceptions and mark the parent dirty
+ final ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
+ final Cursor c = mContentResolver.query(Events.CONTENT_URI,
+ ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION_IN_CALENDAR, calendarIdArgument, null);
+ if (c != null) {
+ try {
+ final ContentValues cv = new ContentValues(1);
+ // We use _sync_mark here to distinguish dirty parents from parents with dirty
+ // exceptions
+ cv.put(EVENT_SYNC_MARK, "1");
+ while (c.moveToNext()) {
+ // Mark the parents of dirty exceptions
+ final long parentId = c.getLong(ORIGINAL_EVENT_ORIGINAL_ID_COLUMN);
+ final int cnt = mContentResolver.update(asSyncAdapter(Events.CONTENT_URI), cv,
+ EVENT_ID_AND_CALENDAR_ID,
+ new String[] { Long.toString(parentId), calendarIdString });
+ // Keep track of any orphaned exceptions
+ if (cnt == 0) {
+ orphanedExceptions.add(c.getLong(ORIGINAL_EVENT_ID_COLUMN));
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Delete any orphaned exceptions
+ for (final long orphan : orphanedExceptions) {
+ LogUtils.i(TAG, "Deleted orphaned exception: %d", orphan);
+ mContentResolver.delete(asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, orphan)), null, null);
+ }
+ }
+
+ /**
+ * Get the version number of the current event, incrementing it if it's already there.
+ * @param entityValues The {@link ContentValues} for this event.
+ * @return The new version number for this event (i.e. 0 if it's a new event, or the old version
+ * number + 1).
+ */
+ private String getEntityVersion(final ContentValues entityValues) {
+ final String version = entityValues.getAsString(EVENT_SYNC_VERSION);
+ // This should never be null, but catch this error anyway
+ // Version should be "0" when we create the event, so use that
+ if (version != null) {
+ // Increment and save
+ try {
+ return Integer.toString((Integer.parseInt(version) + 1));
+ } catch (final NumberFormatException e) {
+ // Handle the case in which someone writes a non-integer here;
+ // shouldn't happen, but we don't want to kill the sync for his
+ }
+ }
+ return "0";
+ }
+
+ /**
+ * Convenience method for sending an email to the organizer declining the meeting.
+ * @param entity The {@link Entity} for this event.
+ * @param clientId The client id for this event.
+ */
+ private void sendDeclinedEmail(final Entity entity, final String clientId) {
+ final Message msg =
+ CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, mAccount);
+ if (msg != null) {
+ LogUtils.i(TAG, "Queueing declined response to %s", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+
+ /**
+ * Get an integer value from a {@link ContentValues}, or 0 if the value isn't there.
+ * @param cv The {@link ContentValues} to find the value in.
+ * @param column The name of the column in cv to get.
+ * @return The appropriate value as an integer, or 0 if it's not there.
+ */
+ private static int getInt(final ContentValues cv, final String column) {
+ final Integer i = cv.getAsInteger(column);
+ if (i == null) return 0;
+ return i;
+ }
+
+ /**
+ * Convert {@link Events} visibility values to EAS visibility values.
+ * @param visibility The {@link Events} visibility value.
+ * @return The corresponding EAS visibility value.
+ */
+ private static String decodeVisibility(final int visibility) {
+ final int easVisibility;
+ switch(visibility) {
+ case Events.ACCESS_DEFAULT:
+ easVisibility = 0;
+ break;
+ case Events.ACCESS_PUBLIC:
+ easVisibility = 1;
+ break;
+ case Events.ACCESS_PRIVATE:
+ easVisibility = 2;
+ break;
+ case Events.ACCESS_CONFIDENTIAL:
+ easVisibility = 3;
+ break;
+ default:
+ easVisibility = 0;
+ break;
+ }
+ return Integer.toString(easVisibility);
+ }
+
+ /**
+ * Write an event to the {@link Serializer} for this upsync.
+ * @param entity The {@link Entity} for this event.
+ * @param clientId The client id for this event.
+ * @param s The {@link Serializer} for this Sync request.
+ * @throws IOException
+ * TODO: This can probably be refactored/cleaned up more.
+ */
+ private void sendEvent(final Entity entity, final String clientId, final Serializer s)
+ throws IOException {
+ // Serialize for EAS here
+ // Set uid with the client id we created
+ // 1) Serialize the top-level event
+ // 2) Serialize attendees and reminders from subvalues
+ // 3) Look for exceptions and serialize with the top-level event
+ final ContentValues entityValues = entity.getEntityValues();
+ final boolean isException = (clientId == null);
+ boolean hasAttendees = false;
+ final boolean isChange = entityValues.containsKey(Events._SYNC_ID);
+ final boolean allDay =
+ CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY);
+ final TimeZone localTimeZone = TimeZone.getDefault();
+
+ // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception
+ // start time" data before other data in exceptions. Failure to do so results in a
+ // status 6 error during sync
+ if (isException) {
+ // Send exception deleted flag if necessary
+ final Integer deleted = entityValues.getAsInteger(Events.DELETED);
+ final boolean isDeleted = deleted != null && deleted == 1;
+ final Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
+ final boolean isCanceled =
+ eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
+ if (isDeleted || isCanceled) {
+ s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1");
+ // If we're deleted, the UI will continue to show this exception until we mark
+ // it canceled, so we'll do that here...
+ if (isDeleted && !isCanceled) {
+ final long eventId = entityValues.getAsLong(Events._ID);
+ final ContentValues cv = new ContentValues(1);
+ cv.put(Events.STATUS, Events.STATUS_CANCELED);
+ mContentResolver.update(
+ asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId)),
+ cv, null, null);
+ }
+ } else {
+ s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0");
+ }
+
+ // TODO Add reminders to exceptions (allow them to be specified!)
+ Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ if (originalTime != null) {
+ final boolean originalAllDay =
+ CalendarUtilities.getIntegerValueAsBoolean(entityValues,
+ Events.ORIGINAL_ALL_DAY);
+ if (originalAllDay) {
+ // For all day events, we need our local all-day time
+ originalTime =
+ CalendarUtilities.getLocalAllDayCalendarTime(originalTime, localTimeZone);
+ }
+ s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+ CalendarUtilities.millisToEasDateTime(originalTime));
+ } else {
+ // Illegal; what should we do?
+ }
+ }
+
+ if (!isException) {
+ // A time zone is required in all EAS events; we'll use the default if none is set
+ // Exchange 2003 seems to require this first... :-)
+ String timeZoneName = entityValues.getAsString(
+ allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE);
+ if (timeZoneName == null) {
+ timeZoneName = localTimeZone.getID();
+ }
+ s.data(Tags.CALENDAR_TIME_ZONE,
+ CalendarUtilities.timeZoneToTziString(TimeZone.getTimeZone(timeZoneName)));
+ }
+
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0");
+
+ // DTSTART is always supplied
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ // Determine endTime; it's either provided as DTEND or we calculate using DURATION
+ // If no DURATION is provided, we default to one hour
+ long endTime;
+ if (entityValues.containsKey(Events.DTEND)) {
+ endTime = entityValues.getAsLong(Events.DTEND);
+ } else {
+ long durationMillis = DateUtils.HOUR_IN_MILLIS;
+ if (entityValues.containsKey(Events.DURATION)) {
+ final Duration duration = new Duration();
+ try {
+ duration.parse(entityValues.getAsString(Events.DURATION));
+ durationMillis = duration.getMillis();
+ } catch (DateException e) {
+ // Can't do much about this; use the default (1 hour)
+ }
+ }
+ endTime = startTime + durationMillis;
+ }
+ if (allDay) {
+ startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, localTimeZone);
+ endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, localTimeZone);
+ }
+ s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime));
+ s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime));
+
+ s.data(Tags.CALENDAR_DTSTAMP,
+ CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+ String loc = entityValues.getAsString(Events.EVENT_LOCATION);
+ if (!TextUtils.isEmpty(loc)) {
+ if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ // EAS 2.5 doesn't like bare line feeds
+ loc = Utility.replaceBareLfWithCrlf(loc);
+ }
+ s.data(Tags.CALENDAR_LOCATION, loc);
+ }
+ s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
+
+ if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, "1");
+ s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA);
+ s.end();
+ } else {
+ // EAS 2.5 doesn't like bare line feeds
+ s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY);
+ }
+
+ if (!isException) {
+ // For Exchange 2003, only upsync if the event is new
+ if ((getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) {
+ s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
+ }
+
+ final String rrule = entityValues.getAsString(Events.RRULE);
+ if (rrule != null) {
+ CalendarUtilities.recurrenceFromRrule(rrule, startTime, s);
+ }
+
+ // Handle associated data EXCEPT for attendees, which have to be grouped
+ final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+ // The earliest of the reminders for this Event; we can only send one reminder...
+ int earliestReminder = -1;
+ for (final Entity.NamedContentValues ncv: subValues) {
+ final Uri ncvUri = ncv.uri;
+ final ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+ final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME);
+ final String propertyValue = ncvValues.getAsString(ExtendedProperties.VALUE);
+ if (TextUtils.isEmpty(propertyValue)) {
+ continue;
+ }
+ if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) {
+ // Send all the categories back to the server
+ // We've saved them as a String of delimited tokens
+ final StringTokenizer st =
+ new StringTokenizer(propertyValue, CATEGORY_TOKENIZER_DELIMITER);
+ if (st.countTokens() > 0) {
+ s.start(Tags.CALENDAR_CATEGORIES);
+ while (st.hasMoreTokens()) {
+ s.data(Tags.CALENDAR_CATEGORY, st.nextToken());
+ }
+ s.end();
+ }
+ }
+ } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
+ Integer mins = ncvValues.getAsInteger(Reminders.MINUTES);
+ if (mins != null) {
+ // -1 means "default", which for Exchange, is 30
+ if (mins < 0) {
+ mins = 30;
+ }
+ // Save this away if it's the earliest reminder (greatest minutes)
+ if (mins > earliestReminder) {
+ earliestReminder = mins;
+ }
+ }
+ }
+ }
+
+ // If we have a reminder, send it to the server
+ if (earliestReminder >= 0) {
+ s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder));
+ }
+
+ // We've got to send a UID, unless this is an exception. If the event is new, we've
+ // generated one; if not, we should have gotten one from extended properties.
+ if (clientId != null) {
+ s.data(Tags.CALENDAR_UID, clientId);
+ }
+
+ // Handle attendee data here; keep track of organizer and stream it afterward
+ String organizerName = null;
+ String organizerEmail = null;
+ for (final Entity.NamedContentValues ncv: subValues) {
+ final Uri ncvUri = ncv.uri;
+ final ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(Attendees.CONTENT_URI)) {
+ final Integer relationship =
+ ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+ // If there's no relationship, we can't create this for EAS
+ // Similarly, we need an attendee email for each invitee
+ if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+ // Organizer isn't among attendees in EAS
+ if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+ organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ continue;
+ }
+ if (!hasAttendees) {
+ s.start(Tags.CALENDAR_ATTENDEES);
+ hasAttendees = true;
+ }
+ s.start(Tags.CALENDAR_ATTENDEE);
+ final String attendeeEmail =
+ ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ if (attendeeName == null) {
+ attendeeName = attendeeEmail;
+ }
+ s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName);
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail);
+ if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+ }
+ s.end(); // Attendee
+ }
+ }
+ }
+ if (hasAttendees) {
+ s.end(); // Attendees
+ }
+
+ // Get busy status from availability
+ final int availability = entityValues.getAsInteger(Events.AVAILABILITY);
+ final int busyStatus = CalendarUtilities.busyStatusFromAvailability(availability);
+ s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
+
+ // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
+ // In JB, organizer won't be an attendee
+ if (organizerEmail == null && entityValues.containsKey(Events.ORGANIZER)) {
+ organizerEmail = entityValues.getAsString(Events.ORGANIZER);
+ }
+ if (mAccount.mEmailAddress.equalsIgnoreCase(organizerEmail)) {
+ s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
+ } else {
+ s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+ }
+
+ // For Exchange 2003, only upsync if the event is new
+ if (((getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) &&
+ organizerName != null) {
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+ }
+
+ // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003
+ // The result will be a status 6 failure during sync
+ final Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL);
+ if (visibility != null) {
+ s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
+ } else {
+ // Default to private if not set
+ s.data(Tags.CALENDAR_SENSITIVITY, "1");
+ }
+ }
+ }
+
+ /**
+ * Handle exceptions to an event's recurrance pattern.
+ * @param s The {@link Serializer} for this upsync.
+ * @param entity The {@link Entity} for this event.
+ * @param entityValues The {@link ContentValues} for entity.
+ * @param serverId The server side id for this event.
+ * @param clientId The client side id for this event.
+ * @param calendarIdString The calendar id, as a {@link String}.
+ * @param selfOrganizer Whether the user is the organizer of this event.
+ * @throws IOException
+ */
+ private void handleExceptionsToRecurrenceRules(final Serializer s, final Entity entity,
+ final ContentValues entityValues, final String serverId, final String clientId,
+ final String calendarIdString, final boolean selfOrganizer) throws IOException {
+ final EntityIterator exIterator = EventsEntity.newEntityIterator(mContentResolver.query(
+ asSyncAdapter(Events.CONTENT_URI), null, ORIGINAL_EVENT_AND_CALENDAR,
+ new String[] { serverId, calendarIdString }, null), mContentResolver);
+ boolean exFirst = true;
+ while (exIterator.hasNext()) {
+ final Entity exEntity = exIterator.next();
+ if (exFirst) {
+ s.start(Tags.CALENDAR_EXCEPTIONS);
+ exFirst = false;
+ }
+ s.start(Tags.CALENDAR_EXCEPTION);
+ sendEvent(exEntity, null, s);
+ final ContentValues exValues = exEntity.getEntityValues();
+ if (getInt(exValues, Events.DIRTY) == 1) {
+ // This is a new/updated exception, so we've got to notify our
+ // attendees about it
+ final long exEventId = exValues.getAsLong(Events._ID);
+
+ // Copy subvalues into the exception; otherwise, we won't see the
+ // attendees when preparing the message
+ for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
+ exEntity.addSubValue(ncv.uri, ncv.values);
+ }
+
+ final int flag;
+ if ((getInt(exValues, Events.DELETED) == 1) ||
+ (getInt(exValues, Events.STATUS) == Events.STATUS_CANCELED)) {
+ flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
+ if (!selfOrganizer) {
+ // Send a cancellation notice to the organizer
+ // Since CalendarProvider2 sets the organizer of exceptions
+ // to the user, we have to reset it first to the original
+ // organizer
+ exValues.put(Events.ORGANIZER, entityValues.getAsString(Events.ORGANIZER));
+ sendDeclinedEmail(exEntity, clientId);
+ }
+ } else {
+ flag = Message.FLAG_OUTGOING_MEETING_INVITE;
+ }
+ // Add the eventId of the exception to the uploaded id list, so that
+ // the dirty/mark bits are cleared
+ mUploadedIdList.add(exEventId);
+
+ // Copy version so the ics attachment shows the proper sequence #
+ exValues.put(EVENT_SYNC_VERSION,
+ entityValues.getAsString(EVENT_SYNC_VERSION));
+ // Copy location so that it's included in the outgoing email
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ exValues.put(Events.EVENT_LOCATION,
+ entityValues.getAsString(Events.EVENT_LOCATION));
+ }
+
+ if (selfOrganizer) {
+ final Message msg = CalendarUtilities.createMessageForEntity(mContext, exEntity,
+ flag, clientId, mAccount);
+ if (msg != null) {
+ LogUtils.i(TAG, "Queueing exception update to %s", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ }
+ s.end(); // EXCEPTION
+ }
+ if (!exFirst) {
+ s.end(); // EXCEPTIONS
+ }
+ }
+
+ /**
+ * Update the event properties with the attendee list, and send mail as appropriate.
+ * @param entity The {@link Entity} for this event.
+ * @param entityValues The {@link ContentValues} for entity.
+ * @param selfOrganizer Whether the user is the organizer of this event.
+ * @param eventId The id for this event.
+ * @param clientId The client side id for this event.
+ */
+ private void updateAttendeesAndSendMail(final Entity entity, final ContentValues entityValues,
+ final boolean selfOrganizer, final long eventId, final String clientId) {
+ // Go through the extended properties of this Event and pull out our tokenized
+ // attendees list and the user attendee status; we will need them later
+ String attendeeString = null;
+ long attendeeStringId = -1;
+ String userAttendeeStatus = null;
+ long userAttendeeStatusId = -1;
+ for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ final ContentValues ncvValues = ncv.values;
+ final String propertyName = ncvValues.getAsString(ExtendedProperties.NAME);
+ if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
+ attendeeString = ncvValues.getAsString(ExtendedProperties.VALUE);
+ attendeeStringId = ncvValues.getAsLong(ExtendedProperties._ID);
+ } else if (propertyName.equals(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
+ userAttendeeStatus = ncvValues.getAsString(ExtendedProperties.VALUE);
+ userAttendeeStatusId = ncvValues.getAsLong(ExtendedProperties._ID);
+ }
+ }
+ }
+
+ // Send the meeting invite if there are attendees and we're the organizer AND
+ // if the Event itself is dirty (we might be syncing only because an exception
+ // is dirty, in which case we DON'T send email about the Event)
+ if (selfOrganizer && (getInt(entityValues, Events.DIRTY) == 1)) {
+ final Message msg =
+ CalendarUtilities.createMessageForEventId(mContext, eventId,
+ Message.FLAG_OUTGOING_MEETING_INVITE, clientId, mAccount);
+ if (msg != null) {
+ LogUtils.i(TAG, "Queueing invitation to %s", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ // Make a list out of our tokenized attendees, if we have any
+ final ArrayList<String> originalAttendeeList = new ArrayList<String>();
+ if (attendeeString != null) {
+ final StringTokenizer st =
+ new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
+ while (st.hasMoreTokens()) {
+ originalAttendeeList.add(st.nextToken());
+ }
+ }
+ final StringBuilder newTokenizedAttendees = new StringBuilder();
+ // See if any attendees have been dropped and while we're at it, build
+ // an updated String with tokenized attendee addresses
+ for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(Attendees.CONTENT_URI)) {
+ final String attendeeEmail = ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
+ // Remove all found attendees
+ originalAttendeeList.remove(attendeeEmail);
+ newTokenizedAttendees.append(attendeeEmail);
+ newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
+ }
+ }
+ // Update extended properties with the new attendee list, if we have one
+ // Otherwise, create one (this would be the case for Events created on
+ // device or "legacy" events (before this code was added)
+ final ContentValues cv = new ContentValues();
+ cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
+ if (attendeeString != null) {
+ mContentResolver.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, attendeeStringId)), cv, null, null);
+ } else {
+ // If there wasn't an "attendees" property, insert one
+ cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
+ cv.put(ExtendedProperties.EVENT_ID, eventId);
+ mContentResolver.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI), cv);
+ }
+ // Whoever is left has been removed from the attendee list; send them
+ // a cancellation
+ for (final String removedAttendee: originalAttendeeList) {
+ // Send a cancellation message to each of them
+ final Message cancelMsg = CalendarUtilities.createMessageForEventId(mContext,
+ eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
+ removedAttendee);
+ if (cancelMsg != null) {
+ // Just send it to the removed attendee
+ LogUtils.i(TAG, "Queueing cancellation to removed attendee %s", cancelMsg.mTo);
+ mOutgoingMailList.add(cancelMsg);
+ }
+ }
+ } else if (!selfOrganizer) {
+ // If we're not the organizer, see if we've changed our attendee status
+ // Our last synced attendee status is in ExtendedProperties, and we've
+ // retrieved it above as userAttendeeStatus
+ final int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
+ int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
+ if (userAttendeeStatus != null) {
+ try {
+ syncStatus = Integer.parseInt(userAttendeeStatus);
+ } catch (NumberFormatException e) {
+ // Just in case somebody else mucked with this and it's not Integer
+ }
+ }
+ if ((currentStatus != syncStatus) &&
+ (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
+ // If so, send a meeting reply
+ final int messageFlag;
+ switch (currentStatus) {
+ case Attendees.ATTENDEE_STATUS_ACCEPTED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
+ break;
+ case Attendees.ATTENDEE_STATUS_DECLINED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
+ break;
+ case Attendees.ATTENDEE_STATUS_TENTATIVE:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
+ break;
+ default:
+ messageFlag = 0;
+ break;
+ }
+ // Make sure we have a valid status (messageFlag should never be zero)
+ if (messageFlag != 0 && userAttendeeStatusId >= 0) {
+ // Save away the new status
+ final ContentValues cv = new ContentValues(1);
+ cv.put(ExtendedProperties.VALUE, Integer.toString(currentStatus));
+ mContentResolver.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, userAttendeeStatusId)),
+ cv, null, null);
+ // Send mail to the organizer advising of the new status
+ final Message msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
+ messageFlag, clientId, mAccount);
+ if (msg != null) {
+ LogUtils.i(TAG, "Queueing invitation reply to %s", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Process a single event, adding to the {@link Serializer} as necessary.
+ * @param s The {@link Serializer} for this Sync request.
+ * @param entity The {@link Entity} for this event.
+ * @param calendarIdString The calendar's id, as a {@link String}.
+ * @param first Whether this would be the first event added to s.
+ * @return Whether this function added anything to s.
+ * @throws IOException
+ */
+ private boolean handleEntity(final Serializer s, final Entity entity,
+ final String calendarIdString, final boolean first) throws IOException {
+ // For each of these entities, create the change commands
+ final ContentValues entityValues = entity.getEntityValues();
+ // We first need to check whether we can upsync this event; our test for this
+ // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
+ // If this is set to "1", we can't upsync the event
+ for (final Entity.NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ final ContentValues ncvValues = ncv.values;
+ if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
+ EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
+ if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
+ // Make sure we mark this to clear the dirty flag
+ mUploadedIdList.add(entityValues.getAsLong(Events._ID));
+ return false;
+ }
+ }
+ }
+ }
+
+ // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+ // We can generate all but what we're testing for below
+ final String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
+ if (organizerEmail == null || !entityValues.containsKey(Events.DTSTART) ||
+ (!entityValues.containsKey(Events.DURATION)
+ && !entityValues.containsKey(Events.DTEND))) {
+ return false;
+ }
+
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ LogUtils.i(TAG, "Sending Calendar changes to the server");
+ }
+
+ final boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mAccount.mEmailAddress);
+ // Find our uid in the entity; otherwise create one
+ String clientId = entityValues.getAsString(Events.SYNC_DATA2);
+ if (clientId == null) {
+ clientId = UUID.randomUUID().toString();
+ }
+ final String serverId = entityValues.getAsString(Events._SYNC_ID);
+ final long eventId = entityValues.getAsLong(Events._ID);
+ if (serverId == null) {
+ // This is a new event; create a clientId
+ LogUtils.i(TAG, "Creating new event with clientId: %s", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the Event as the local id
+ final ContentValues cv = new ContentValues(2);
+ cv.put(Events.SYNC_DATA2, clientId);
+ cv.put(EVENT_SYNC_VERSION, "0");
+ mContentResolver.update(
+ asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId)),
+ cv, null, null);
+ } else if (entityValues.getAsInteger(Events.DELETED) == 1) {
+ LogUtils.i(TAG, "Deleting event with serverId: %s", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(eventId);
+ if (selfOrganizer) {
+ final Message msg = CalendarUtilities.createMessageForEventId(mContext,
+ eventId, Message.FLAG_OUTGOING_MEETING_CANCEL, null, mAccount);
+ if (msg != null) {
+ LogUtils.i(TAG, "Queueing cancellation to %s", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ } else {
+ sendDeclinedEmail(entity, clientId);
+ }
+ // For deletions, we don't need to add application data, so just bail here.
+ return true;
+ } else {
+ LogUtils.i(TAG, "Upsync change to event with serverId: %s", serverId);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ // Save to the ContentResolver.
+ final String version = getEntityVersion(entityValues);
+ final ContentValues cv = new ContentValues(1);
+ cv.put(EVENT_SYNC_VERSION, version);
+ mContentResolver.update(
+ asSyncAdapter( ContentUris.withAppendedId(Events.CONTENT_URI, eventId)),
+ cv, null, null);
+ // Also save in entityValues so that we send it this time around
+ entityValues.put(EVENT_SYNC_VERSION, version);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ sendEvent(entity, clientId, s);
+
+ // Now, the hard part; find exceptions for this event
+ if (serverId != null) {
+ handleExceptionsToRecurrenceRules(s, entity, entityValues, serverId, clientId,
+ calendarIdString, selfOrganizer);
+ }
+
+ s.end().end(); // ApplicationData & Add/Change
+ mUploadedIdList.add(eventId);
+ updateAttendeesAndSendMail(entity, entityValues, selfOrganizer, eventId, clientId);
+ return true;
+ }
+
@Override
protected void setUpsyncCommands(final Serializer s) throws IOException {
+ final String calendarIdString = Long.toString(mCalendarId);
+ final String[] calendarIdArgument = { calendarIdString };
+ markParentsOfDirtyEvents(calendarIdString, calendarIdArgument);
+
+ // Now go through dirty/marked top-level events and send them back to the server
+ final EntityIterator eventIterator = EventsEntity.newEntityIterator(
+ mContentResolver.query(asSyncAdapter(Events.CONTENT_URI), null,
+ DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, calendarIdArgument, null), mContentResolver);
+
+ try {
+ boolean first = true;
+ while (eventIterator.hasNext()) {
+ final boolean addedCommand =
+ handleEntity(s, eventIterator.next(), calendarIdString, first);
+ if (addedCommand) {
+ first = false;
+ }
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ eventIterator.close();
+ }
}
@Override
protected void cleanup(final int syncResult) {
- // Nothing to do.
+ if (syncResult != SYNC_RESULT_FAILED) {
+ // Clear dirty and mark flags for updates sent to server
+ if (!mUploadedIdList.isEmpty()) {
+ final ContentValues cv = new ContentValues(2);
+ cv.put(Events.DIRTY, 0);
+ cv.put(EVENT_SYNC_MARK, "0");
+ for (final long eventId : mUploadedIdList) {
+ mContentResolver.update(asSyncAdapter(ContentUris.withAppendedId(
+ Events.CONTENT_URI, eventId)), cv, null, null);
+ }
+ }
+ // Delete events marked for deletion
+ if (!mDeletedIdList.isEmpty()) {
+ for (final long eventId : mDeletedIdList) {
+ mContentResolver.delete(asSyncAdapter(ContentUris.withAppendedId(
+ Events.CONTENT_URI, eventId)), null, null);
+ }
+ }
+ // Send all messages that were created during this sync.
+ for (final Message msg : mOutgoingMailList) {
+ sendMessage(msg);
+ }
+ }
+ // Clear our lists for the next Sync request, if necessary.
+ if (syncResult != SYNC_RESULT_MORE_AVAILABLE) {
+ mDeletedIdList.clear();
+ mUploadedIdList.clear();
+ mOutgoingMailList.clear();
+ }
}
}
diff --git a/src/com/android/exchange/service/EasServerConnection.java b/src/com/android/exchange/service/EasServerConnection.java
index b87e128..f1b5e32 100644
--- a/src/com/android/exchange/service/EasServerConnection.java
+++ b/src/com/android/exchange/service/EasServerConnection.java
@@ -12,6 +12,7 @@
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.EmailClientConnectionManager;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
@@ -360,4 +361,19 @@
mStopped = true;
}
}
+
+ /**
+ * Convenience method for adding a Message to an account's outbox
+ * @param msg the message to send
+ */
+ protected void sendMessage(final EmailContent.Message msg) {
+ final Mailbox mailbox =
+ Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX);
+ if (mailbox != null) {
+ msg.mMailboxKey = mailbox.mId;
+ msg.mAccountKey = mAccount.mId;
+ msg.save(mContext);
+ }
+ }
+
}
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index eca3f0a..7efe769 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -2005,18 +2005,15 @@
* there aren't any addressees; if false, return the Message
* regardless (addressees will be filled in later)
* @return a Message with many fields pre-filled (more later)
- * @throws RemoteException if there is an issue retrieving the Event from
- * CalendarProvider
*/
static public EmailContent.Message createMessageForEventId(Context context, long eventId,
- int messageFlag, String uid, Account account) throws RemoteException {
+ int messageFlag, String uid, Account account) {
return createMessageForEventId(context, eventId, messageFlag, uid, account,
null /* specifiedAttendee */);
}
static public EmailContent.Message createMessageForEventId(Context context, long eventId,
- int messageFlag, String uid, Account account, String specifiedAttendee)
- throws RemoteException {
+ int messageFlag, String uid, Account account, String specifiedAttendee) {
ContentResolver cr = context.getContentResolver();
EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
ContentUris.withAppendedId(Events.CONTENT_URI, eventId), null, null, null, null),