content: /*
   * Copyright (C) 2007 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.calendarcommon2;
  
  import android.content.ContentValues;
  import android.database.Cursor;
  import android.provider.CalendarContract;
  import android.text.TextUtils;
  import android.text.format.Time;
  import android.util.Log;
  import android.util.TimeFormatException;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.regex.Pattern;
  
  /**
   * Basic information about a recurrence, following RFC 2445 Section 4.8.5.
   * Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.
   */
  public class RecurrenceSet {
  
      private final static String TAG = "RecurrenceSet";
  
      private final static String RULE_SEPARATOR = "\n";
      private final static String FOLDING_SEPARATOR = "\n ";
  
      // TODO: make these final?
      public EventRecurrence[] rrules = null;
      public long[] rdates = null;
      public EventRecurrence[] exrules = null;
      public long[] exdates = null;
  
      /**
       * Creates a new RecurrenceSet from information stored in the
       * events table in the CalendarProvider.
       * @param values The values retrieved from the Events table.
       */
      public RecurrenceSet(ContentValues values)
              throws EventRecurrence.InvalidFormatException {
          String rruleStr = values.getAsString(CalendarContract.Events.RRULE);
          String rdateStr = values.getAsString(CalendarContract.Events.RDATE);
          String exruleStr = values.getAsString(CalendarContract.Events.EXRULE);
          String exdateStr = values.getAsString(CalendarContract.Events.EXDATE);
          init(rruleStr, rdateStr, exruleStr, exdateStr);
      }
  
      /**
       * Creates a new RecurrenceSet from information stored in a database
       * {@link Cursor} pointing to the events table in the
       * CalendarProvider.  The cursor must contain the RRULE, RDATE, EXRULE,
       * and EXDATE columns.
       *
       * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE
       * columns.
       */
      public RecurrenceSet(Cursor cursor)
              throws EventRecurrence.InvalidFormatException {
          int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE);
          int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE);
          int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE);
          int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE);
          String rruleStr = cursor.getString(rruleColumn);
          String rdateStr = cursor.getString(rdateColumn);
          String exruleStr = cursor.getString(exruleColumn);
          String exdateStr = cursor.getString(exdateColumn);
          init(rruleStr, rdateStr, exruleStr, exdateStr);
      }
  
      public RecurrenceSet(String rruleStr, String rdateStr,
                    String exruleStr, String exdateStr)
              throws EventRecurrence.InvalidFormatException {
          init(rruleStr, rdateStr, exruleStr, exdateStr);
      }
  
      private void init(String rruleStr, String rdateStr,
                        String exruleStr, String exdateStr)
              throws EventRecurrence.InvalidFormatException {
          if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {
              rrules = parseMultiLineRecurrenceRules(rruleStr);
              rdates = parseMultiLineRecurrenceDates(rdateStr);
              exrules = parseMultiLineRecurrenceRules(exruleStr);
              exdates = parseMultiLineRecurrenceDates(exdateStr);
          }
      }
  
      private EventRecurrence[] parseMultiLineRecurrenceRules(String ruleStr) {
          if (TextUtils.isEmpty(ruleStr)) {
              return null;
          }
          String[] ruleStrs = ruleStr.split(RULE_SEPARATOR);
          final EventRecurrence[] rules = new EventRecurrence[ruleStrs.length];
          for (int i = 0; i < ruleStrs.length; ++i) {
              EventRecurrence rule = new EventRecurrence();
              rule.parse(ruleStrs[i]);
              rules[i] = rule;
          }
          return rules;
      }
  
      private long[] parseMultiLineRecurrenceDates(String dateStr) {
          if (TextUtils.isEmpty(dateStr)) {
              return null;
          }
          final List<Long> list = new ArrayList<>();
          for (String date : dateStr.split(RULE_SEPARATOR)) {
              final long[] parsedDates = parseRecurrenceDates(date);
              for (long parsedDate : parsedDates) {
                  list.add(parsedDate);
              }
          }
          final long[] result = new long[list.size()];
          for (int i = 0, n = list.size(); i < n; i++) {
              result[i] = list.get(i);
          }
          return result;
      }
  
      /**
       * Returns whether or not a recurrence is defined in this RecurrenceSet.
       * @return Whether or not a recurrence is defined in this RecurrenceSet.
       */
      public boolean hasRecurrence() {
          return (rrules != null || rdates != null);
      }
  
      /**
       * Parses the provided RDATE or EXDATE string into an array of longs
       * representing each date/time in the recurrence.
       * @param recurrence The recurrence to be parsed.
       * @return The list of date/times.
       */
      public static long[] parseRecurrenceDates(String recurrence)
              throws EventRecurrence.InvalidFormatException{
          // TODO: use "local" time as the default.  will need to handle times
          // that end in "z" (UTC time) explicitly at that point.
          String tz = Time.TIMEZONE_UTC;
          int tzidx = recurrence.indexOf(";");
          if (tzidx != -1) {
              tz = recurrence.substring(0, tzidx);
              recurrence = recurrence.substring(tzidx + 1);
          }
          Time time = new Time(tz);
          String[] rawDates = recurrence.split(",");
          int n = rawDates.length;
          long[] dates = new long[n];
          for (int i = 0; i<n; ++i) {
              // The timezone is updated to UTC if the time string specified 'Z'.
              try {
                  time.parse(rawDates[i]);
              } catch (TimeFormatException e) {
                  throw new EventRecurrence.InvalidFormatException(
                          "TimeFormatException thrown when parsing time " + rawDates[i]
                                  + " in recurrence " + recurrence);
  
              }
              dates[i] = time.toMillis(false /* use isDst */);
              time.timezone = tz;
          }
          return dates;
      }
  
      /**
       * Populates the database map of values with the appropriate RRULE, RDATE,
       * EXRULE, and EXDATE values extracted from the parsed iCalendar component.
       * @param component The iCalendar component containing the desired
       * recurrence specification.
       * @param values The db values that should be updated.
       * @return true if the component contained the necessary information
       * to specify a recurrence.  The required fields are DTSTART,
       * one of DTEND/DURATION, and one of RRULE/RDATE.  Returns false if
       * there was an error, including if the date is out of range.
       */
      public static boolean populateContentValues(ICalendar.Component component,
              ContentValues values) {
          try {
              ICalendar.Property dtstartProperty =
                      component.getFirstProperty("DTSTART");
              String dtstart = dtstartProperty.getValue();
              ICalendar.Parameter tzidParam =
                      dtstartProperty.getFirstParameter("TZID");
              // NOTE: the timezone may be null, if this is a floating time.
              String tzid = tzidParam == null ? null : tzidParam.value;
              Time start = new Time(tzidParam == null ? Time.TIMEZONE_UTC : tzid);
              boolean inUtc = start.parse(dtstart);
              boolean allDay = start.allDay;
  
              // We force TimeZone to UTC for "all day recurring events" as the server is sending no
              // TimeZone in DTSTART for them
              if (inUtc || allDay) {
                  tzid = Time.TIMEZONE_UTC;
              }
  
              String duration = computeDuration(start, component);
              String rrule = flattenProperties(component, "RRULE");
              String rdate = extractDates(component.getFirstProperty("RDATE"));
              String exrule = flattenProperties(component, "EXRULE");
              String exdate = extractDates(component.getFirstProperty("EXDATE"));
  
              if ((TextUtils.isEmpty(dtstart))||
                      (TextUtils.isEmpty(duration))||
                      ((TextUtils.isEmpty(rrule))&&
                              (TextUtils.isEmpty(rdate)))) {
                      if (false) {
                          Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, "
                                      + "or RRULE/RDATE: "
                                      + component.toString());
                      }
                      return false;
              }
  
              if (allDay) {
                  start.timezone = Time.TIMEZONE_UTC;
              }
              long millis = start.toMillis(false /* use isDst */);
              values.put(CalendarContract.Events.DTSTART, millis);
              if (millis == -1) {
                  if (false) {
                      Log.d(TAG, "DTSTART is out of range: " + component.toString());
                  }
                  return false;
              }
  
              values.put(CalendarContract.Events.RRULE, rrule);
              values.put(CalendarContract.Events.RDATE, rdate);
              values.put(CalendarContract.Events.EXRULE, exrule);
              values.put(CalendarContract.Events.EXDATE, exdate);
              values.put(CalendarContract.Events.EVENT_TIMEZONE, tzid);
              values.put(CalendarContract.Events.DURATION, duration);
              values.put(CalendarContract.Events.ALL_DAY, allDay ? 1 : 0);
              return true;
          } catch (TimeFormatException e) {
              // Something is wrong with the format of this event
              Log.i(TAG,"Failed to parse event: " + component.toString());
              return false;
          }
      }
  
      // This can be removed when the old CalendarSyncAdapter is removed.
      public static boolean populateComponent(Cursor cursor,
                                              ICalendar.Component component) {
  
          int dtstartColumn = cursor.getColumnIndex(CalendarContract.Events.DTSTART);
          int durationColumn = cursor.getColumnIndex(CalendarContract.Events.DURATION);
          int tzidColumn = cursor.getColumnIndex(CalendarContract.Events.EVENT_TIMEZONE);
          int rruleColumn = cursor.getColumnIndex(CalendarContract.Events.RRULE);
          int rdateColumn = cursor.getColumnIndex(CalendarContract.Events.RDATE);
          int exruleColumn = cursor.getColumnIndex(CalendarContract.Events.EXRULE);
          int exdateColumn = cursor.getColumnIndex(CalendarContract.Events.EXDATE);
          int allDayColumn = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY);
  
  
          long dtstart = -1;
          if (!cursor.isNull(dtstartColumn)) {
              dtstart = cursor.getLong(dtstartColumn);
          }
          String duration = cursor.getString(durationColumn);
          String tzid = cursor.getString(tzidColumn);
          String rruleStr = cursor.getString(rruleColumn);
          String rdateStr = cursor.getString(rdateColumn);
          String exruleStr = cursor.getString(exruleColumn);
          String exdateStr = cursor.getString(exdateColumn);
          boolean allDay = cursor.getInt(allDayColumn) == 1;
  
          if ((dtstart == -1) ||
              (TextUtils.isEmpty(duration))||
              ((TextUtils.isEmpty(rruleStr))&&
                  (TextUtils.isEmpty(rdateStr)))) {
                  // no recurrence.
                  return false;
          }
  
          ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
          Time dtstartTime = null;
          if (!TextUtils.isEmpty(tzid)) {
              if (!allDay) {
                  dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
              }
              dtstartTime = new Time(tzid);
          } else {
              // use the "floating" timezone
              dtstartTime = new Time(Time.TIMEZONE_UTC);
          }
  
          dtstartTime.set(dtstart);
          // make sure the time is printed just as a date, if all day.
          // TODO: android.pim.Time really should take care of this for us.
          if (allDay) {
              dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
              dtstartTime.allDay = true;
              dtstartTime.hour = 0;
              dtstartTime.minute = 0;
              dtstartTime.second = 0;
          }
  
          dtstartProp.setValue(dtstartTime.format2445());
          component.addProperty(dtstartProp);
          ICalendar.Property durationProp = new ICalendar.Property("DURATION");
          durationProp.setValue(duration);
          component.addProperty(durationProp);
  
          addPropertiesForRuleStr(component, "RRULE", rruleStr);
          addPropertyForDateStr(component, "RDATE", rdateStr);
          addPropertiesForRuleStr(component, "EXRULE", exruleStr);
          addPropertyForDateStr(component, "EXDATE", exdateStr);
          return true;
      }
  
  public static boolean populateComponent(ContentValues values,
                                              ICalendar.Component component) {
          long dtstart = -1;
          if (values.containsKey(CalendarContract.Events.DTSTART)) {
              dtstart = values.getAsLong(CalendarContract.Events.DTSTART);
          }
          final String duration = values.getAsString(CalendarContract.Events.DURATION);
          final String tzid = values.getAsString(CalendarContract.Events.EVENT_TIMEZONE);
          final String rruleStr = values.getAsString(CalendarContract.Events.RRULE);
          final String rdateStr = values.getAsString(CalendarContract.Events.RDATE);
          final String exruleStr = values.getAsString(CalendarContract.Events.EXRULE);
          final String exdateStr = values.getAsString(CalendarContract.Events.EXDATE);
          final Integer allDayInteger = values.getAsInteger(CalendarContract.Events.ALL_DAY);
          final boolean allDay = (null != allDayInteger) ? (allDayInteger == 1) : false;
  
          if ((dtstart == -1) ||
              (TextUtils.isEmpty(duration))||
              ((TextUtils.isEmpty(rruleStr))&&
                  (TextUtils.isEmpty(rdateStr)))) {
                  // no recurrence.
                  return false;
          }
  
          ICalendar.Property dtstartProp = new ICalendar.Property("DTSTART");
          Time dtstartTime = null;
          if (!TextUtils.isEmpty(tzid)) {
              if (!allDay) {
                  dtstartProp.addParameter(new ICalendar.Parameter("TZID", tzid));
              }
              dtstartTime = new Time(tzid);
          } else {
              // use the "floating" timezone
              dtstartTime = new Time(Time.TIMEZONE_UTC);
          }
  
          dtstartTime.set(dtstart);
          // make sure the time is printed just as a date, if all day.
          // TODO: android.pim.Time really should take care of this for us.
          if (allDay) {
              dtstartProp.addParameter(new ICalendar.Parameter("VALUE", "DATE"));
              dtstartTime.allDay = true;
              dtstartTime.hour = 0;
              dtstartTime.minute = 0;
              dtstartTime.second = 0;
          }
  
          dtstartProp.setValue(dtstartTime.format2445());
          component.addProperty(dtstartProp);
          ICalendar.Property durationProp = new ICalendar.Property("DURATION");
          durationProp.setValue(duration);
          component.addProperty(durationProp);
  
          addPropertiesForRuleStr(component, "RRULE", rruleStr);
          addPropertyForDateStr(component, "RDATE", rdateStr);
          addPropertiesForRuleStr(component, "EXRULE", exruleStr);
          addPropertyForDateStr(component, "EXDATE", exdateStr);
          return true;
      }
  
      public static void addPropertiesForRuleStr(ICalendar.Component component,
                                                  String propertyName,
                                                  String ruleStr) {
          if (TextUtils.isEmpty(ruleStr)) {
              return;
          }
          String[] rrules = getRuleStrings(ruleStr);
          for (String rrule : rrules) {
              ICalendar.Property prop = new ICalendar.Property(propertyName);
              prop.setValue(rrule);
              component.addProperty(prop);
          }
      }
  
      private static String[] getRuleStrings(String ruleStr) {
          if (null == ruleStr) {
              return new String[0];
          }
          String unfoldedRuleStr = unfold(ruleStr);
          String[] split = unfoldedRuleStr.split(RULE_SEPARATOR);
          int count = split.length;
          for (int n = 0; n < count; n++) {
              split[n] = fold(split[n]);
          }
          return split;
      }
  
  
      private static final Pattern IGNORABLE_ICAL_WHITESPACE_RE =
              Pattern.compile("(?:\\r\\n?|\\n)[ \t]");
  
      private static final Pattern FOLD_RE = Pattern.compile(".{75}");
  
      /**
      * fold and unfolds ical content lines as per RFC 2445 section 4.1.
      *
      * <h3>4.1 Content Lines</h3>
      *
      * <p>The iCalendar object is organized into individual lines of text, called
      * content lines. Content lines are delimited by a line break, which is a CRLF
      * sequence (US-ASCII decimal 13, followed by US-ASCII decimal 10).
      *
      * <p>Lines of text SHOULD NOT be longer than 75 octets, excluding the line
      * break. Long content lines SHOULD be split into a multiple line
      * representations using a line "folding" technique. That is, a long line can
      * be split between any two characters by inserting a CRLF immediately
      * followed by a single linear white space character (i.e., SPACE, US-ASCII
      * decimal 32 or HTAB, US-ASCII decimal 9). Any sequence of CRLF followed
      * immediately by a single linear white space character is ignored (i.e.,
      * removed) when processing the content type.
      */
      public static String fold(String unfoldedIcalContent) {
          return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n ");
      }
  
      public static String unfold(String foldedIcalContent) {
          return IGNORABLE_ICAL_WHITESPACE_RE.matcher(
              foldedIcalContent).replaceAll("");
      }
  
      public static void addPropertyForDateStr(ICalendar.Component component,
                                                String propertyName,
                                                String dateStr) {
          if (TextUtils.isEmpty(dateStr)) {
              return;
          }
  
          ICalendar.Property prop = new ICalendar.Property(propertyName);
          String tz = null;
          int tzidx = dateStr.indexOf(";");
          if (tzidx != -1) {
              tz = dateStr.substring(0, tzidx);
              dateStr = dateStr.substring(tzidx + 1);
          }
          if (!TextUtils.isEmpty(tz)) {
              prop.addParameter(new ICalendar.Parameter("TZID", tz));
          }
          prop.setValue(dateStr);
          component.addProperty(prop);
      }
  
      private static String computeDuration(Time start,
                                            ICalendar.Component component) {
          // see if a duration is defined
          ICalendar.Property durationProperty =
                  component.getFirstProperty("DURATION");
          if (durationProperty != null) {
              // just return the duration
              return durationProperty.getValue();
          }
  
          // must compute a duration from the DTEND
          ICalendar.Property dtendProperty =
                  component.getFirstProperty("DTEND");
          if (dtendProperty == null) {
              // no DURATION, no DTEND: 0 second duration
              return "+P0S";
          }
          ICalendar.Parameter endTzidParameter =
                  dtendProperty.getFirstParameter("TZID");
          String endTzid = (endTzidParameter == null)
                  ? start.timezone : endTzidParameter.value;
  
          Time end = new Time(endTzid);
          end.parse(dtendProperty.getValue());
          long durationMillis = end.toMillis(false /* use isDst */)
                  - start.toMillis(false /* use isDst */);
          long durationSeconds = (durationMillis / 1000);
          if (start.allDay && (durationSeconds % 86400) == 0) {
              return "P" + (durationSeconds / 86400) + "D"; // Server wants this instead of P86400S
          } else {
              return "P" + durationSeconds + "S";
          }
      }
  
      private static String flattenProperties(ICalendar.Component component,
                                              String name) {
          List<ICalendar.Property> properties = component.getProperties(name);
          if (properties == null || properties.isEmpty()) {
              return null;
          }
  
          if (properties.size() == 1) {
              return properties.get(0).getValue();
          }
  
          StringBuilder sb = new StringBuilder();
  
          boolean first = true;
          for (ICalendar.Property property : component.getProperties(name)) {
              if (first) {
                  first = false;
              } else {
                  // TODO: use commas.  our RECUR parsing should handle that
                  // anyway.
                  sb.append(RULE_SEPARATOR);
              }
              sb.append(property.getValue());
          }
          return sb.toString();
      }
  
      private static String extractDates(ICalendar.Property recurrence) {
          if (recurrence == null) {
              return null;
          }
          ICalendar.Parameter tzidParam =
                  recurrence.getFirstParameter("TZID");
          if (tzidParam != null) {
              return tzidParam.value + ";" + recurrence.getValue();
          }
          return recurrence.getValue();
      }
  }
  