From dc6930c0b36950d420c8da0e70164c5c4e083fbe Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Fri, 29 Jan 2010 13:33:34 -0800 Subject: [PATCH] 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 --- .../exchange/adapter/CalendarSyncAdapter.java | 156 ++++++++++++------ .../exchange/adapter/FolderSyncParser.java | 2 +- .../exchange/utility/CalendarUtilities.java | 132 ++++++++++++++- .../android/exchange/utility/Duration.java | 128 ++++++++++++++ 4 files changed, 360 insertions(+), 58 deletions(-) create mode 100644 src/com/android/exchange/utility/Duration.java diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java index 12cd8dbc6..f5459b548 100644 --- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java +++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java @@ -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)); } diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java index bd3aaf7ea..57aafcfb1 100644 --- a/src/com/android/exchange/adapter/FolderSyncParser.java +++ b/src/com/android/exchange/adapter/FolderSyncParser.java @@ -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); diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java index 1007d6050..40733260d 100644 --- a/src/com/android/exchange/utility/CalendarUtilities.java +++ b/src/com/android/exchange/utility/CalendarUtilities.java @@ -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(); } -} +} \ No newline at end of file diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java new file mode 100644 index 000000000..0ec867c8a --- /dev/null +++ b/src/com/android/exchange/utility/Duration.java @@ -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); + } +}