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);
+    }
+}