Additional work on new Event upload to EAS server

* Added support for reminders and recurrences
* Note that Duration class is copied from CalendarProvider with only
  formatting changes

Change-Id: Icf399df422f813ba8e7880646bfbc96a2156a204
This commit is contained in:
Marc Blank 2010-01-29 13:33:34 -08:00
parent d99dbf01fb
commit dc6930c0b3
4 changed files with 360 additions and 58 deletions

View File

@ -21,6 +21,7 @@ import com.android.email.provider.EmailContent.Mailbox;
import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
import com.android.exchange.utility.CalendarUtilities;
import com.android.exchange.utility.Duration;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
@ -34,6 +35,7 @@ import android.content.Entity.NamedContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.pim.DateException;
import android.provider.Calendar;
import android.provider.Calendar.Attendees;
import android.provider.Calendar.Calendars;
@ -47,6 +49,7 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.TimeZone;
/**
@ -69,6 +72,8 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
private static final int CALENDAR_SELECTION_ID = 0;
private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
private static final ContentProviderOperation PLACEHOLDER_OPERATION =
ContentProviderOperation.newInsert(Uri.EMPTY).build();
@ -173,7 +178,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
@Override
public void wipe() {
mContentResolver.delete(mAccountUri, null, null);
// 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, Eas.ACCOUNT_MANAGER_TYPE});
}
public void addEvent(CalendarOperations ops, String serverId, boolean update)
@ -243,9 +251,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
case Tags.CALENDAR_BODY:
cv.put(Events.DESCRIPTION, getValue());
break;
case Tags.CALENDAR_CATEGORIES:
categoriesParser(ops);
break;
case Tags.CALENDAR_TIME_ZONE:
TimeZone tz = CalendarUtilities.parseTimeZone(getValue());
if (tz != null) {
@ -255,12 +260,12 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
break;
case Tags.CALENDAR_START_TIME:
startTime = CalendarUtilities.parseDateTime(getValue());
startTime = CalendarUtilities.parseDateTimeToMillis(getValue());
cv.put(Events.DTSTART, startTime);
cv.put(Events.ORIGINAL_INSTANCE_TIME, startTime);
break;
case Tags.CALENDAR_END_TIME:
endTime = CalendarUtilities.parseDateTime(getValue());
endTime = CalendarUtilities.parseDateTimeToMillis(getValue());
break;
case Tags.CALENDAR_EXCEPTIONS:
exceptionsParser(ops, cv);
@ -284,26 +289,32 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
case Tags.CALENDAR_SENSITIVITY:
cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
break;
case Tags.CALENDAR_UID:
ops.newExtendedProperty("uid", getValue());
break;
case Tags.CALENDAR_ORGANIZER_NAME:
organizerName = getValue();
break;
case Tags.CALENDAR_REMINDER_MINS_BEFORE:
ops.newReminder(getValueInt());
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:
ops.newExtendedProperty("uid", getValue());
break;
case Tags.CALENDAR_DTSTAMP:
ops.newExtendedProperty("dtstamp", getValue());
break;
case Tags.CALENDAR_MEETING_STATUS:
// TODO Try to fit this into Calendar scheme
ops.newExtendedProperty("meeting_status", getValue());
break;
case Tags.CALENDAR_BUSY_STATUS:
// TODO Try to fit this into Calendar scheme
ops.newExtendedProperty("busy_status", getValue());
break;
case Tags.CALENDAR_REMINDER_MINS_BEFORE:
ops.newReminder(getValueInt());
cv.put(Events.HAS_ALARM, 1);
case Tags.CALENDAR_CATEGORIES:
String categories = categoriesParser(ops);
if (categories.length() > 0) {
ops.newExtendedProperty("categories", categories);
}
break;
default:
skipTag();
@ -399,7 +410,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
switch (tag) {
case Tags.CALENDAR_EXCEPTION_START_TIME:
cv.put(Events.ORIGINAL_INSTANCE_TIME,
CalendarUtilities.parseDateTime(getValue()));
CalendarUtilities.parseDateTimeToMillis(getValue()));
break;
case Tags.CALENDAR_EXCEPTION_IS_DELETED:
if (getValueInt() == 1) {
@ -416,10 +427,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
cv.put(Events.DESCRIPTION, getValue());
break;
case Tags.CALENDAR_START_TIME:
cv.put(Events.DTSTART, CalendarUtilities.parseDateTime(getValue()));
cv.put(Events.DTSTART, CalendarUtilities.parseDateTimeToMillis(getValue()));
break;
case Tags.CALENDAR_END_TIME:
cv.put(Events.DTEND, CalendarUtilities.parseDateTime(getValue()));
cv.put(Events.DTEND, CalendarUtilities.parseDateTimeToMillis(getValue()));
break;
case Tags.CALENDAR_LOCATION:
cv.put(Events.EVENT_LOCATION, getValue());
@ -500,15 +511,20 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
}
private void categoriesParser(CalendarOperations ops) throws IOException {
private String categoriesParser(CalendarOperations ops) throws IOException {
StringBuilder categories = new StringBuilder();
while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
switch (tag) {
case Tags.CALENDAR_CATEGORY:
// TODO Handle categories
// 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);
default:
skipTag();
}
}
return categories.toString();
}
private String attendeesParser(CalendarOperations ops, String organizerName,
@ -918,11 +934,21 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
boolean first = true;
while (ei.hasNext()) {
Entity entity = ei.next();
String clientId = null;
String clientId = "uid_" + mMailbox.mId + '_' + System.currentTimeMillis();
// For each of these entities, create the change commands
ContentValues entityValues = entity.getEntityValues();
String serverId = entityValues.getAsString(Events._SYNC_ID);
// EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
// We can generate all but what we're testing for below
if (!entityValues.containsKey(Events.DTSTART)
|| !entityValues.containsKey(Events.DURATION)) {
continue;
}
// TODO Handle BusyStatus for EAS 2.5
// What should it be??
// Ignore exceptions (will have Events.ORIGINAL_EVENT)
if (first) {
@ -932,7 +958,6 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
if (serverId == null) {
// This is a new event; create a clientId
clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
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
@ -963,29 +988,41 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
s.data(Tags.CALENDAR_ALL_DAY_EVENT,
entityValues.getAsInteger(Events.ALL_DAY).toString());
}
if (entityValues.containsKey(Events.DTSTART)) {
long startTime = entityValues.getAsLong(Events.DTSTART);
s.data(Tags.CALENDAR_START_TIME,
CalendarUtilities.millisToEasDateTime(startTime));
long startTime = entityValues.getAsLong(Events.DTSTART);
s.data(Tags.CALENDAR_START_TIME,
CalendarUtilities.millisToEasDateTime(startTime));
// Convert this into millis and add it to DTSTART for DTEND
// We'll use 1 hour as a default
long durationMillis = HOURS;
Duration duration = new Duration();
try {
duration.parse(entityValues.getAsString(Events.DURATION));
} catch (DateException e) {
// Can't do much about this; use the default (1 hour)
}
s.data(Tags.CALENDAR_END_TIME,
CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
if (entityValues.containsKey(Events.DTEND)) {
long endTime = entityValues.getAsLong(Events.DTEND);
s.data(Tags.CALENDAR_END_TIME,
CalendarUtilities.millisToEasDateTime(endTime));
// TODO Use this to determine last date; it's NOT the same as EAS DTEND
//long endTime = entityValues.getAsLong(Events.DTEND);
//s.data(Tags.CALENDAR_END_TIME,
// CalendarUtilities.millisToEasDateTime(endTime));
}
s.data(Tags.CALENDAR_DTSTAMP,
CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
// Our clientId (for new calendar items) is used for UID
if (clientId != null) {
s.data(Tags.CALENDAR_UID, clientId);
}
// A time zone is required in all EAS events; we'll use the default if none
// is set.
String timeZoneName;
if (entityValues.containsKey(Events.EVENT_TIMEZONE)) {
String timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
String x = CalendarUtilities.timeZoneToTZIString(timeZoneName);
s.data(Tags.CALENDAR_TIME_ZONE, x);
timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
} else {
timeZoneName = TimeZone.getDefault().getID();
}
String x = CalendarUtilities.timeZoneToTZIString(timeZoneName);
s.data(Tags.CALENDAR_TIME_ZONE, x);
if (entityValues.containsKey(Events.EVENT_LOCATION)) {
s.data(Tags.CALENDAR_LOCATION,
entityValues.getAsString(Events.EVENT_LOCATION));
@ -1011,6 +1048,13 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
if (entityValues.containsKey(Events.VISIBILITY)) {
s.data(Tags.CALENDAR_SENSITIVITY,
decodeVisibility(entityValues.getAsInteger(Events.VISIBILITY)));
} else {
// Private if not set
s.data(Tags.CALENDAR_SENSITIVITY, "1");
}
if (entityValues.containsKey(Events.RRULE)) {
CalendarUtilities.recurrenceFromRrule(
entityValues.getAsString(Events.RRULE), startTime, s);
}
// Handle associated data EXCEPT for attendees, which have to be grouped
@ -1020,11 +1064,27 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
ContentValues ncvValues = ncv.values;
if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
if (ncvValues.containsKey("uid")) {
s.data(Tags.CALENDAR_UID, ncvValues.getAsString("uid"));
clientId = ncvValues.getAsString("uid");
s.data(Tags.CALENDAR_UID, clientId);
}
if (ncvValues.containsKey("dtstamp")) {
s.data(Tags.CALENDAR_DTSTAMP, ncvValues.getAsString("dtstamp"));
}
if (ncvValues.containsKey("categories")) {
// Send all the categories back to the server
// We've saved them as a String of delimited tokens
String categories = ncvValues.getAsString("categories");
StringTokenizer st =
new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
if (st.countTokens() > 0) {
s.start(Tags.CALENDAR_CATEGORIES);
while (st.hasMoreTokens()) {
String category = st.nextToken();
s.data(Tags.CALENDAR_CATEGORY, category);
}
s.end();
}
}
} else if (ncvUri.equals(Reminders.CONTENT_URI)) {
if (ncvValues.containsKey(Reminders.MINUTES)) {
s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE,
@ -1033,6 +1093,10 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
}
}
// We've got to send a UID. If the event is new, we've generated one; if not,
// we should have gotten one from extended properties.
s.data(Tags.CALENDAR_UID, clientId);
// Handle attendee data here; keep track of organizer and stream it afterward
boolean hasAttendees = false;
String organizerName = null;
@ -1078,27 +1142,9 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
if (organizerName != null) {
s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
}
// case Tags.CALENDAR_CATEGORIES:
// categoriesParser(ops);
// break;
// case Tags.CALENDAR_EXCEPTIONS:
// exceptionsParser(ops, cv);
// break;
// case Tags.CALENDAR_RECURRENCE:
// String rrule = recurrenceParser(ops);
// if (rrule != null) {
// cv.put(Events.RRULE, rrule);
// }
// break;
// case Tags.CALENDAR_MEETING_STATUS:
// // TODO Try to fit this into Calendar scheme
// ops.newExtendedProperty("meeting_status", getValue());
// break;
// case Tags.CALENDAR_BUSY_STATUS:
// // TODO Try to fit this into Calendar scheme
// ops.newExtendedProperty("busy_status", getValue());
// break;
s.end().end(); // ApplicationData & Change
mUpdatedIdList.add(entityValues.getAsLong(Events._ID));
}

View File

@ -254,7 +254,7 @@ public class FolderSyncParser extends AbstractSyncParser {
cv.put(Calendars.SELECTED, 1);
cv.put(Calendars.HIDDEN, 0);
// TODO Find out how to set color!!
cv.put(Calendars.COLOR, -14069085 /* blue */);
cv.put(Calendars.COLOR, 0xFF228B22 /*green*/);
cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
cv.put(Calendars.OWNER_ACCOUNT, mAccount.mEmailAddress);

View File

@ -17,11 +17,14 @@
package com.android.exchange.utility;
import com.android.exchange.Eas;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import org.bouncycastle.util.encoders.Base64;
import android.util.Log;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -318,7 +321,7 @@ public class CalendarUtilities {
* @param DateTime string from Exchange server
* @return the time in milliseconds (since Jan 1, 1970)
*/
static public long parseDateTime(String date) {
static public long parseDateTimeToMillis(String date) {
// Format for calendar date strings is 20090211T180303Z
GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
@ -328,6 +331,21 @@ public class CalendarUtilities {
return cal.getTimeInMillis();
}
/**
* Generate a GregorianCalendar from a date string that represents a date/time in GMT
* @param DateTime string from Exchange server
* @return the GregorianCalendar
*/
static public GregorianCalendar parseDateTimeToCalendar(String date) {
// Format for calendar date strings is 20090211T180303Z
GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
Integer.parseInt(date.substring(13, 15)));
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
return cal;
}
static String formatTwo(int num) {
if (num <= 12) {
return sTwoCharacterNumbers[num];
@ -378,6 +396,116 @@ public class CalendarUtilities {
rrule.append(";BYMONTHDAY=" + dom);
}
static String generateEasDayOfWeek(String dow) {
int bit = 1;
for (String token: sDayTokens) {
if (dow.equals(token)) {
break;
} else {
bit <<= 1;
}
}
return Integer.toString(bit);
}
static String tokenFromRrule(String rrule, String token) {
int start = rrule.indexOf(token);
if (start < 0) return null;
int len = rrule.length();
start += token.length();
int end = start;
char c;
do {
c = rrule.charAt(end++);
if (!Character.isLetterOrDigit(c) || (end == len)) {
if (end == len) end++;
return rrule.substring(start, end -1);
}
} while (true);
}
/**
* Write recurrence information to EAS based on the RRULE in CalendarProvider
* @param rrule the RRULE, from CalendarProvider
* @param startTime, the DTSTART of this Event
* @param s the Serializer we're using to write WBXML data
* @throws IOException
*/
// NOTE: For the moment, we're only parsing recurrence types that are supported by the
// Calendar app UI, which is a small subset of possible recurrence types
// This code must be updated when the Calendar adds new functionality
static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
throws IOException {
Log.d("RRULE", "rule: " + rrule);
String freq = tokenFromRrule(rrule, "FREQ=");
// If there's no FREQ=X, then we don't write a recurrence
// Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
// possibility of writing out a partial recurrence stanza
if (freq != null) {
if (freq.equals("DAILY")) {
s.start(Tags.CALENDAR_RECURRENCE);
s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
s.end();
} else if (freq.equals("WEEKLY")) {
s.start(Tags.CALENDAR_RECURRENCE);
s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
// Requires a day of week (whereas RRULE does not)
String byDay = tokenFromRrule(rrule, "BYDAY=");
if (byDay != null) {
s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
}
s.end();
} else if (freq.equals("MONTHLY")) {
String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
if (byMonthDay != null) {
// The nth day of the month
s.start(Tags.CALENDAR_RECURRENCE);
s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
s.end();
} else {
String byDay = tokenFromRrule(rrule, "BYDAY=");
String bareByDay;
if (byDay != null) {
// This can be 1WE (1st Wednesday) or -1FR (last Friday)
int wom = byDay.charAt(0);
if (wom == '-') {
// -1 is the only legal case (last week) Use "5" for EAS
wom = 5;
bareByDay = byDay.substring(2);
} else {
wom = wom - '0';
bareByDay = byDay.substring(1);
}
s.start(Tags.CALENDAR_RECURRENCE);
s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom));
s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
s.end();
}
}
} else if (freq.equals("YEARLY")) {
String byMonth = tokenFromRrule(rrule, "BYMONTH=");
String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
if (byMonth == null || byMonthDay == null) {
// Calculate the month and day from the startDate
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(startTime);
cal.setTimeZone(TimeZone.getDefault());
byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
}
s.start(Tags.CALENDAR_RECURRENCE);
s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5");
s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
s.end();
}
}
}
static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
int dom, int wom, int moy, String until) {
StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
@ -426,4 +554,4 @@ public class CalendarUtilities {
return rrule.toString();
}
}
}

View File

@ -0,0 +1,128 @@
/* Copyright 2010, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package com.android.exchange.utility;
import android.pim.DateException;
import java.util.Calendar;
/**
* Note: This class was simply copied from the class in CalendarProvider, since we don't have access
* to it from the Email app. I reformated some lines, but otherwise haven't altered the code.
*/
public class Duration {
public int sign; // 1 or -1
public int weeks;
public int days;
public int hours;
public int minutes;
public int seconds;
public Duration() {
sign = 1;
}
/**
* Parse according to RFC2445 ss4.3.6. (It's actually a little loose with
* its parsing, for better or for worse)
*/
public void parse(String str) throws DateException {
sign = 1;
weeks = 0;
days = 0;
hours = 0;
minutes = 0;
seconds = 0;
int len = str.length();
int index = 0;
char c;
if (len < 1) {
return;
}
c = str.charAt(0);
if (c == '-') {
sign = -1;
index++;
} else if (c == '+') {
index++;
}
if (len < index) {
return;
}
c = str.charAt(index);
if (c != 'P') {
throw new DateException (
"Duration.parse(str='" + str + "') expected 'P' at index="
+ index);
}
index++;
int n = 0;
for (; index < len; index++) {
c = str.charAt(index);
if (c >= '0' && c <= '9') {
n *= 10;
n += (c - '0');
} else if (c == 'W') {
weeks = n;
n = 0;
} else if (c == 'H') {
hours = n;
n = 0;
} else if (c == 'M') {
minutes = n;
n = 0;
} else if (c == 'S') {
seconds = n;
n = 0;
} else if (c == 'D') {
days = n;
n = 0;
} else if (c == 'T') {
} else {
throw new DateException (
"Duration.parse(str='" + str + "') unexpected char '"
+ c + "' at index=" + index);
}
}
}
/**
* Add this to the calendar provided, in place, in the calendar.
*/
public void addTo(Calendar cal) {
cal.add(Calendar.DAY_OF_MONTH, sign*weeks*7);
cal.add(Calendar.DAY_OF_MONTH, sign*days);
cal.add(Calendar.HOUR, sign*hours);
cal.add(Calendar.MINUTE, sign*minutes);
cal.add(Calendar.SECOND, sign*seconds);
}
public long addTo(long dt) {
return dt + getMillis();
}
public long getMillis() {
long factor = 1000 * sign;
return factor * ((7*24*60*60*weeks) + (24*60*60*days) + (60*60*hours) + (60*minutes) +
seconds);
}
}