Skip to content

Commit c88d9a1

Browse files
authored
Fix recurring events with unsupported rrules. (#1465)
Fix recurring events with unsupported rrules. The android calendar provider does not support RRULE's with BYSETPOS or BYWEEKNO properly, so additional processing has to be done to ensure that duplicate events are not added to the calendar.
1 parent a2cdaf7 commit c88d9a1

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ dependencies {
114114
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
115115
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
116116

117+
// https://mvnrepository.com/artifact/org.dmfs/lib-recur
118+
implementation 'org.dmfs:lib-recur:0.15.0'
119+
117120
}
118121

119122
preBuild.dependsOn (":aarGen")

app/src/main/java/com/android/calendar/Event.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,15 @@
3434

3535
import com.android.calendar.settings.GeneralPreferences;
3636

37+
import org.dmfs.rfc5545.DateTime;
38+
import org.dmfs.rfc5545.iterable.RecurrenceSet;
39+
import org.dmfs.rfc5545.iterable.instanceiterable.RuleInstances;
40+
import org.dmfs.rfc5545.recur.RecurrenceRule;
41+
42+
import java.text.SimpleDateFormat;
3743
import java.util.ArrayList;
3844
import java.util.Arrays;
45+
import java.util.Date;
3946
import java.util.Iterator;
4047
import java.util.concurrent.atomic.AtomicInteger;
4148

@@ -368,6 +375,22 @@ private static Event generateEventFromCursor(Cursor cEvents, Context context) {
368375
String rdate = cEvents.getString(PROJECTION_RDATE_INDEX);
369376
if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)) {
370377
e.isRepeating = true;
378+
379+
/** We need to double check a few RRULE conditions that the Android Calendar Provider
380+
* doesn't handle and shows duplicate events for, namely:
381+
*
382+
* - BYSETPOS
383+
* - BYWEEKNO
384+
*
385+
* For these conditions, double check if this event really occurs on this day, if it
386+
* doesn't, reset the endDay value to 0 so it is removed from the events list.
387+
*
388+
* It might make sense to check all rrule's, as there may be other broken sets, but
389+
* the overhead is probably not worth it at this point.
390+
**/
391+
if (rrule.contains("BYSETPOS=") || rrule.contains("BYWEEKNO=")) {
392+
e.endDay = checkRRuleEventDate(rrule, e.startMillis, e.endDay);
393+
}
371394
} else {
372395
e.isRepeating = false;
373396
}
@@ -376,6 +399,56 @@ private static Event generateEventFromCursor(Cursor cEvents, Context context) {
376399
return e;
377400
}
378401

402+
/** Android's RRULE code is broken in a way the creates additional events in certain
403+
* circumstances (though never doesn't create the actual event) so let's use another RRULE
404+
* parser to validate if the event is real or not.
405+
*
406+
* In this case we're using lib-recur from https://github.com/dmfs/lib-recur through maven.
407+
*
408+
**/
409+
static int checkRRuleEventDate( String rrule, long startTime, int endDay) {
410+
// Convert the startTime into some useable Day/Month/Year values.
411+
Date date = new java.util.Date(startTime);
412+
413+
// We'll use SimpleDateFormat to get the D/M/Y but we also need to set the timezone.
414+
SimpleDateFormat sdf = new java.text.SimpleDateFormat();
415+
sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
416+
417+
sdf.applyPattern("yyyy");
418+
int startYear = Integer.parseInt(sdf.format(date));
419+
sdf.applyPattern("MM");
420+
int startMonth = Integer.parseInt(sdf.format(date)) - 1;
421+
sdf.applyPattern("dd");
422+
int startDay = Integer.parseInt(sdf.format(date));
423+
424+
// Parse the recurrence rule.
425+
RecurrenceRule rule;
426+
try {
427+
rule = new RecurrenceRule(rrule);
428+
} catch (Exception e) {
429+
// On failure, assume we match and return.
430+
return endDay;
431+
}
432+
433+
// Use the Year/Month/Day startTime values to create a firstInstance.
434+
DateTime firstInstance = new DateTime(startYear, startMonth, startDay);
435+
436+
// Create the recurrence set for the rule, we're only going to look at the first one
437+
// as it should match the firstInstance if this is a valid event from Android.
438+
for (DateTime instance:new RecurrenceSet(firstInstance, new RuleInstances(rule))) {
439+
if (!instance.equals(firstInstance)) {
440+
// If this isn't a valid event, return 0 so it gets removed from the event list.
441+
return 0;
442+
} else {
443+
// If this is a valid event, return the endDay that we were passed in with.
444+
return endDay;
445+
}
446+
}
447+
448+
// We should never get here, but add a return just in case.
449+
return endDay;
450+
}
451+
379452
/**
380453
* Computes a position for each event. Each event is displayed
381454
* as a non-overlapping rectangle. For normal events, these rectangles

0 commit comments

Comments
 (0)