Fix upload/download of attendee status

* It turns out that the UI uses selfAttendeeStatus and the attendee's status
  from the Attendees table in confusing and undocumented ways
* selfAttendeeStatus is used in the UI, but only in certain cases. Generally speaking,
  the Attendees table status is definitive.  However, when the user sets his status
  from the UI, this data is reflected in the event's selfAttendeeStatus, since for EAS,
  the user is always the owner of his calendar
* On downsync,  we'll put the user's busy status into the Attendees table
* On upsync, we'll send busy status based on the user's attendee status in the
  Attendees table
* We'll use selfAttendeeStatus only to determine whether the user has manually changed
  his status via the UI (as before)

Bug: 2615586
Change-Id: I3a82474cfd07cbf5aa595e5214807cb55005cefa
This commit is contained in:
Marc Blank 2010-04-22 09:55:40 -07:00
parent 6cfa8001a8
commit 8a19af3739
3 changed files with 100 additions and 50 deletions

View File

@ -90,6 +90,13 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
private static final String[] ORIGINAL_EVENT_PROJECTION =
new String[] {Events.ORIGINAL_EVENT, Events._ID};
// Note that we use LIKE below for its case insensitivity
private static final String EVENT_AND_EMAIL =
Attendees.EVENT_ID + "=? AND "+ Attendees.ATTENDEE_EMAIL + " LIKE ?";
private static final int ATTENDEE_STATUS_COLUMN_STATUS = 0;
private static final String[] ATTENDEE_STATUS_PROJECTION =
new String[] {Attendees.ATTENDEE_STATUS};
public static final String CALENDAR_SELECTION =
Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
private static final int CALENDAR_SELECTION_ID = 0;
@ -113,6 +120,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
private long mCalendarId = -1;
private String mCalendarIdString;
private String[] mCalendarIdArgument;
private String mEmailAddress;
private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
@ -121,10 +129,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
super(mailbox, service);
mEmailAddress = mAccount.mEmailAddress;
Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
new String[] {Calendars._ID}, CALENDAR_SELECTION,
new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
new String[] {mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
try {
if (c.moveToFirst()) {
mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
@ -241,7 +249,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
// Delete the calendar associated with this account
// TODO Make sure the Events, etc. are also deleted
mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
new String[] {mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
}
private void addOrganizerToAttendees(CalendarOperations ops, long eventId,
@ -257,6 +265,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
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 {
@ -269,7 +278,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
throws IOException {
ContentValues cv = new ContentValues();
cv.put(Events.CALENDAR_ID, mCalendarId);
cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
cv.put(Events._SYNC_ACCOUNT, mEmailAddress);
cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
cv.put(Events._SYNC_ID, serverId);
cv.put(Events.HAS_ATTENDEE_DATA, 1);
@ -280,6 +289,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
String organizerEmail = null;
int eventOffset = -1;
int deleteOffset = -1;
int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
boolean firstTag = true;
long eventId = -1;
@ -370,7 +380,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
// we call exceptionsParser
addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
organizerAdded = true;
exceptionsParser(ops, cv, attendeeValues, reminderMins);
exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus);
break;
case Tags.CALENDAR_LOCATION:
cv.put(Events.EVENT_LOCATION, getValue());
@ -411,9 +421,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
ops.newExtendedProperty("meeting_status", getValue());
break;
case Tags.CALENDAR_BUSY_STATUS:
int busyStatus = getValueInt();
cv.put(Events.SELF_ATTENDEE_STATUS,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(busyStatus));
// 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_CATEGORIES:
String categories = categoriesParser(ops);
@ -432,11 +443,24 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
// 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)
if (attendeeValues.size() > 0) {
StringBuilder sb = new StringBuilder();
for (ContentValues attendee: attendeeValues) {
sb.append(attendee.getAsString(Attendees.ATTENDEE_EMAIL));
String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL);
sb.append(attendeeEmail);
sb.append(ATTENDEE_TOKENIZER_DELIMITER);
if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
attendee.put(Attendees.ATTENDEE_STATUS,
CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
}
if (eventId < 0) {
ops.newAttendee(attendee);
} else {
ops.updatedAttendee(attendee, eventId);
}
}
ops.newExtendedProperty("attendees", sb.toString());
}
@ -568,10 +592,11 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
private void exceptionParser(CalendarOperations ops, ContentValues parentCv,
ArrayList<ContentValues> attendeeValues, int reminderMins) throws IOException {
ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus)
throws IOException {
ContentValues cv = new ContentValues();
cv.put(Events.CALENDAR_ID, mCalendarId);
cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
cv.put(Events._SYNC_ACCOUNT, mEmailAddress);
cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
// It appears that these values have to be copied from the parent if they are to appear
@ -585,7 +610,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
// This column is the key that links the exception to the serverId
// TODO Make sure calendar knows this isn't globally unique!!
cv.put(Events.ORIGINAL_EVENT, parentCv.getAsString(Events._SYNC_ID));
String exceptionStartTime = "_noStartTime";
@ -632,11 +656,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
break;
case Tags.CALENDAR_BUSY_STATUS:
int busyStatus = getValueInt();
cv.put(Events.SELF_ATTENDEE_STATUS,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(busyStatus));
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());
@ -666,6 +689,12 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
// Also add the attendees, because they need to be copied over from the parent event
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);
if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
attValues.put(Attendees.ATTENDEE_STATUS,
CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
}
ops.newAttendee(attValues, exceptionStart);
}
}
@ -695,11 +724,12 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
private void exceptionsParser(CalendarOperations ops, ContentValues cv,
ArrayList<ContentValues> attendeeValues, int reminderMins) throws IOException {
ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus)
throws IOException {
while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
switch (tag) {
case Tags.CALENDAR_EXCEPTION:
exceptionParser(ops, cv, attendeeValues, reminderMins);
exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus);
break;
default:
skipTag();
@ -782,11 +812,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
}
cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
if (eventId < 0) {
ops.newAttendee(cv);
} else {
ops.updatedAttendee(cv, eventId);
}
return cv;
}
@ -1182,13 +1207,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
}
// Busy status is only required for 2.5, but we need to send it with later
// versions as well, because if we don't, the server will clear it.
int selfAttendeeStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
s.data(Tags.CALENDAR_BUSY_STATUS,
Integer.toString(CalendarUtilities
.busyStatusFromSelfAttendeeStatus(selfAttendeeStatus)));
boolean allDay = false;
if (entityValues.containsKey(Events.ALL_DAY)) {
Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
@ -1365,8 +1383,26 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
s.end(); // Attendees
}
// Get busy status from Attendees table
long eventId = entityValues.getAsLong(Events._ID);
int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
Cursor c = mService.mContentResolver.query(ATTENDEES_URI,
ATTENDEE_STATUS_PROJECTION, EVENT_AND_EMAIL,
new String[] {Long.toString(eventId), mEmailAddress}, null);
if (c != null) {
try {
if (c.moveToFirst()) {
busyStatus = CalendarUtilities.busyStatusFromAttendeeStatus(
c.getInt(ATTENDEE_STATUS_COLUMN_STATUS));
}
} finally {
c.close();
}
}
s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
// Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
if (organizerEmail.equalsIgnoreCase(mAccount.mEmailAddress)) {
if (mEmailAddress.equalsIgnoreCase(organizerEmail)) {
s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
} else {
s.data(Tags.CALENDAR_MEETING_STATUS, "3");
@ -1388,9 +1424,21 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
// Send exception deleted flag if necessary
Integer deleted = entityValues.getAsInteger(Events.DELETED);
boolean isDeleted = deleted != null && deleted == 1;
Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
if (eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED)) {
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) {
long eventId = entityValues.getAsLong(Events._ID);
ContentValues cv = new ContentValues();
cv.put(Events.STATUS, Events.STATUS_CANCELED);
mService.mContentResolver.update(
ContentUris.withAppendedId(EVENTS_URI, eventId), cv, null, null);
}
}
}
}
@ -1455,7 +1503,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
cr.query(EVENTS_URI, null, DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR,
mCalendarIdArgument, null), cr);
ContentValues cidValues = new ContentValues();
String ourEmailAddress = mAccount.mEmailAddress;
try {
boolean first = true;
@ -1475,7 +1522,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
// 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(ourEmailAddress);
boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
if (!entityValues.containsKey(Events.DTSTART)
|| (!entityValues.containsKey(Events.DURATION) &&
@ -1707,6 +1754,9 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
} else if (!selfOrganizer) {
// If we're not the organizer, see if we've changed our attendee status
// Note: Since we "own" our own calendar, selfAttendeeStatus will be
// correct when we set it from the UI. We NEVER set selfAttendeeStatus
// on downsync, however.
int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
String adapterData = entityValues.getAsString(Events.SYNC_ADAPTER_DATA);
int syncStatus = Attendees.ATTENDEE_STATUS_NONE;

View File

@ -1228,21 +1228,21 @@ public class CalendarUtilities {
* @param busyStatus the busy status, from EAS
* @return the corresponding value for selfAttendeeStatus
*/
static public int selfAttendeeStatusFromBusyStatus(int busyStatus) {
int selfAttendeeStatus;
static public int attendeeStatusFromBusyStatus(int busyStatus) {
int attendeeStatus;
switch (busyStatus) {
case BUSY_STATUS_BUSY:
selfAttendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
break;
case BUSY_STATUS_TENTATIVE:
selfAttendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
break;
case BUSY_STATUS_FREE:
case BUSY_STATUS_OUT_OF_OFFICE:
default:
selfAttendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
}
return selfAttendeeStatus;
return attendeeStatus;
}
/** Get a busy status from a selfAttendeeStatus
@ -1250,7 +1250,7 @@ public class CalendarUtilities {
* @param selfAttendeeStatus from CalendarProvider2
* @return the corresponding value of busy status
*/
static public int busyStatusFromSelfAttendeeStatus(int selfAttendeeStatus) {
static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
int busyStatus;
switch (selfAttendeeStatus) {
case Attendees.ATTENDEE_STATUS_DECLINED:

View File

@ -717,34 +717,34 @@ public class CalendarUtilitiesTests extends AndroidTestCase {
public void testSelfAttendeeStatusFromBusyStatus() {
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(
CalendarUtilities.attendeeStatusFromBusyStatus(
CalendarUtilities.BUSY_STATUS_BUSY));
assertEquals(Attendees.ATTENDEE_STATUS_TENTATIVE,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(
CalendarUtilities.attendeeStatusFromBusyStatus(
CalendarUtilities.BUSY_STATUS_TENTATIVE));
assertEquals(Attendees.ATTENDEE_STATUS_NONE,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(
CalendarUtilities.attendeeStatusFromBusyStatus(
CalendarUtilities.BUSY_STATUS_FREE));
assertEquals(Attendees.ATTENDEE_STATUS_NONE,
CalendarUtilities.selfAttendeeStatusFromBusyStatus(
CalendarUtilities.attendeeStatusFromBusyStatus(
CalendarUtilities.BUSY_STATUS_OUT_OF_OFFICE));
}
public void testBusyStatusFromSelfStatus() {
assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
CalendarUtilities.busyStatusFromSelfAttendeeStatus(
CalendarUtilities.busyStatusFromAttendeeStatus(
Attendees.ATTENDEE_STATUS_DECLINED));
assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
CalendarUtilities.busyStatusFromSelfAttendeeStatus(
CalendarUtilities.busyStatusFromAttendeeStatus(
Attendees.ATTENDEE_STATUS_NONE));
assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
CalendarUtilities.busyStatusFromSelfAttendeeStatus(
CalendarUtilities.busyStatusFromAttendeeStatus(
Attendees.ATTENDEE_STATUS_INVITED));
assertEquals(CalendarUtilities.BUSY_STATUS_TENTATIVE,
CalendarUtilities.busyStatusFromSelfAttendeeStatus(
CalendarUtilities.busyStatusFromAttendeeStatus(
Attendees.ATTENDEE_STATUS_TENTATIVE));
assertEquals(CalendarUtilities.BUSY_STATUS_BUSY,
CalendarUtilities.busyStatusFromSelfAttendeeStatus(
CalendarUtilities.busyStatusFromAttendeeStatus(
Attendees.ATTENDEE_STATUS_ACCEPTED));
}
}