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
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 12cd8db..f5459b5 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -21,6 +21,7 @@
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.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 java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.StringTokenizer;
import java.util.TimeZone;
/**
@@ -69,6 +72,8 @@
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 @@
@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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
}
}
+ // 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 @@
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 bd3aaf7..57aafcf 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -254,7 +254,7 @@
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 1007d60..4073326 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 @@
* @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 @@
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 @@
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 @@
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 0000000..0ec867c
--- /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);
+ }
+}