FileDocCategorySizeDatePackage
RecurrenceSet.javaAPI DocAndroid 5.1 API22218Thu Mar 12 22:22:50 GMT 2015com.android.calendarcommon2

RecurrenceSet

public class RecurrenceSet extends Object
Basic information about a recurrence, following RFC 2445 Section 4.8.5. Contains the RRULEs, RDATE, EXRULEs, and EXDATE properties.

Fields Summary
private static final String
TAG
private static final String
RULE_SEPARATOR
private static final String
FOLDING_SEPARATOR
public EventRecurrence[]
rrules
public long[]
rdates
public EventRecurrence[]
exrules
public long[]
exdates
private static final Pattern
IGNORABLE_ICAL_WHITESPACE_RE
private static final Pattern
FOLD_RE
Constructors Summary
public RecurrenceSet(android.content.ContentValues values)
Creates a new RecurrenceSet from information stored in the events table in the CalendarProvider.

param
values The values retrieved from the Events table.


                                
      
              
        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);
    
public RecurrenceSet(android.database.Cursor cursor)
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.

        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)

        init(rruleStr, rdateStr, exruleStr, exdateStr);
    
Methods Summary
public static voidaddPropertiesForRuleStr(ICalendar.Component component, java.lang.String propertyName, java.lang.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);
        }
    
public static voidaddPropertyForDateStr(ICalendar.Component component, java.lang.String propertyName, java.lang.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 java.lang.StringcomputeDuration(android.text.format.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 java.lang.StringextractDates(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();
    
private static java.lang.StringflattenProperties(ICalendar.Component component, java.lang.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();
    
public static java.lang.Stringfold(java.lang.String unfoldedIcalContent)
fold and unfolds ical content lines as per RFC 2445 section 4.1.

4.1 Content Lines

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).

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.


                                                                                                                                                
         
        return FOLD_RE.matcher(unfoldedIcalContent).replaceAll("$0\r\n ");
    
private static java.lang.String[]getRuleStrings(java.lang.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;
    
public booleanhasRecurrence()
Returns whether or not a recurrence is defined in this RecurrenceSet.

return
Whether or not a recurrence is defined in this RecurrenceSet.

        return (rrules != null || rdates != null);
    
private voidinit(java.lang.String rruleStr, java.lang.String rdateStr, java.lang.String exruleStr, java.lang.String exdateStr)

        if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) {

            if (!TextUtils.isEmpty(rruleStr)) {
                String[] rruleStrs = rruleStr.split(RULE_SEPARATOR);
                rrules = new EventRecurrence[rruleStrs.length];
                for (int i = 0; i < rruleStrs.length; ++i) {
                    EventRecurrence rrule = new EventRecurrence();
                    rrule.parse(rruleStrs[i]);
                    rrules[i] = rrule;
                }
            }

            if (!TextUtils.isEmpty(rdateStr)) {
                rdates = parseRecurrenceDates(rdateStr);
            }

            if (!TextUtils.isEmpty(exruleStr)) {
                String[] exruleStrs = exruleStr.split(RULE_SEPARATOR);
                exrules = new EventRecurrence[exruleStrs.length];
                for (int i = 0; i < exruleStrs.length; ++i) {
                    EventRecurrence exrule = new EventRecurrence();
                    exrule.parse(exruleStr);
                    exrules[i] = exrule;
                }
            }

            if (!TextUtils.isEmpty(exdateStr)) {
                final List<Long> list = new ArrayList<Long>();
                for (String exdate : exdateStr.split(RULE_SEPARATOR)) {
                    final long[] dates = parseRecurrenceDates(exdate);
                    for (long date : dates) {
                        list.add(date);
                    }
                }
                exdates = new long[list.size()];
                for (int i = 0, n = list.size(); i < n; i++) {
                    exdates[i] = list.get(i);
                }
            }
        }
    
public static long[]parseRecurrenceDates(java.lang.String recurrence)
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.

        // 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;
    
public static booleanpopulateComponent(android.database.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 booleanpopulateComponent(android.content.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 booleanpopulateContentValues(ICalendar.Component component, android.content.ContentValues values)
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.

        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;
        }
    
public static java.lang.Stringunfold(java.lang.String foldedIcalContent)

        return IGNORABLE_ICAL_WHITESPACE_RE.matcher(
            foldedIcalContent).replaceAll("");