3434
3535import 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 ;
3743import java .util .ArrayList ;
3844import java .util .Arrays ;
45+ import java .util .Date ;
3946import java .util .Iterator ;
4047import 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