From c4a97d2569827a2d6e199778f11e8859408f887c Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 4 Feb 2021 17:11:02 -0600 Subject: [PATCH] Use ical4j instead of google-rfc-2445 --- app/build.gradle.kts | 3 - app/licenses.yml | 6 - app/proguard.pro | 3 - .../todoroo/astrid/service/TitleParserTest.kt | 30 +-- .../org/tasks/ui/editviewmodel/RepeatTests.kt | 2 +- .../java/org/tasks/makers/TaskMaker.kt | 4 +- app/src/main/assets/licenses.json | 14 -- .../astrid/activity/TaskListFragment.kt | 10 +- .../main/java/com/todoroo/astrid/data/Task.kt | 23 +- .../astrid/repeats/RepeatControlSet.kt | 31 +-- .../astrid/repeats/RepeatTaskHelper.kt | 197 +++++++----------- .../com/todoroo/astrid/service/TaskCreator.kt | 3 +- .../com/todoroo/astrid/utility/TitleParser.kt | 21 +- .../main/java/org/tasks/caldav/iCalendar.kt | 32 ++- .../preferences/fragments/TaskDefaults.kt | 4 +- .../tasks/repeats/BasicRecurrenceDialog.java | 54 ++--- .../tasks/repeats/CustomRecurrenceDialog.java | 106 +++++----- .../java/org/tasks/repeats/RecurrenceUtils.kt | 3 +- .../org/tasks/repeats/RepeatRuleToString.kt | 8 +- .../main/java/org/tasks/time/DateTime.java | 46 ++-- .../java/org/tasks/ui/TaskEditViewModel.kt | 21 +- .../org/tasks/caldav/AppleRemindersTests.kt | 2 +- .../java/org/tasks/caldav/ThunderbirdTests.kt | 2 +- deps_fdroid.txt | 1 - deps_googleplay.txt | 1 - 25 files changed, 276 insertions(+), 351 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 984e9ccd3..6fa929c3f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -190,9 +190,6 @@ dependencies { implementation("com.rubiconproject.oss:jchronic:0.2.6") { isTransitive = false } - implementation("org.scala-saddle:google-rfc-2445:20110304") { - isTransitive = false - } implementation("com.wdullaer:materialdatetimepicker:4.2.3") implementation("me.leolin:ShortcutBadger:1.1.22@aar") implementation("com.google.apis:google-api-services-tasks:v1-rev20200905-1.30.10") diff --git a/app/licenses.yml b/app/licenses.yml index 91c641991..3c64e49c8 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -156,12 +156,6 @@ license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt url: https://developer.android.com/topic/libraries/architecture/index.html -- artifact: org.scala-saddle:google-rfc-2445:+ - name: google-rfc-2445 - copyrightHolder: Google Inc. - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://code.google.com/p/google-rfc-2445/ - artifact: com.google.dagger:dagger:+ name: Dagger copyrightHolder: The Dagger Authors diff --git a/app/proguard.pro b/app/proguard.pro index 0bd0b79e5..284dd997a 100644 --- a/app/proguard.pro +++ b/app/proguard.pro @@ -9,9 +9,6 @@ public static *** i(...); } -# google-rfc-2445-20110304 --dontwarn com.google.ical.compat.jodatime.** - # https://github.com/JakeWharton/butterknife/blob/581666a28022796fdd62caaf3420e621215abfda/butterknife/proguard-rules.txt -keep public class * implements butterknife.Unbinder { public (**, android.view.View); } -keep class butterknife.* diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/TitleParserTest.kt b/app/src/androidTest/java/com/todoroo/astrid/service/TitleParserTest.kt index 28b4eb473..6035ade94 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/TitleParserTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/service/TitleParserTest.kt @@ -283,19 +283,19 @@ class TitleParserTest : InjectingTestCase() { val recur = newRecur() recur.setFrequency(DAILY.name) recur.interval = 1 - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) title = "Jog every day" task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) for (i in 1..12) { title = "Jog every $i days." recur.interval = i task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) } @@ -309,19 +309,19 @@ class TitleParserTest : InjectingTestCase() { val recur = newRecur() recur.setFrequency(WEEKLY.name) recur.interval = 1 - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) title = "Jog every week" task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) for (i in 1..12) { title = "Jog every $i weeks" recur.interval = i task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) } @@ -335,19 +335,19 @@ class TitleParserTest : InjectingTestCase() { val recur = newRecur() recur.setFrequency(MONTHLY.name) recur.interval = 1 - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) title = "Jog every month" task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) for (i in 1..12) { title = "Jog every $i months" recur.interval = i task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertFalse(task.hasDueTime()) assertFalse(task.hasDueDate()) } @@ -360,17 +360,17 @@ class TitleParserTest : InjectingTestCase() { val recur = newRecur() recur.setFrequency(DAILY.name) recur.interval = 1 - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) title = "Jog every day starting from today" task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) for (i in 1..12) { title = "Jog every $i days starting from today" recur.interval = i task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) } } @@ -382,17 +382,17 @@ class TitleParserTest : InjectingTestCase() { val recur = newRecur() recur.setFrequency(WEEKLY.name) recur.interval = 1 - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) title = "Jog every week starting from today" task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) for (i in 1..12) { title = "Jog every $i weeks starting from today" recur.interval = i task = taskCreator.createWithValues(title) - assertEquals(task.recurrence, "RRULE:$recur") + assertEquals(task.recurrence, recur.toString()) assertTrue(task.hasDueDate()) } } diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt index 899bcc3bd..deb24b9e8 100644 --- a/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/RepeatTests.kt @@ -23,7 +23,7 @@ class RepeatTests : BaseTaskEditViewModelTest() { save() assertEquals( - "RRULE:FREQ=DAILY;INTERVAL=1;FROM=COMPLETION", + "FREQ=DAILY;INTERVAL=1;FROM=COMPLETION", taskDao.fetch(task.id)!!.recurrence) } diff --git a/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt b/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt index d0e874dec..b1835dd5f 100644 --- a/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt +++ b/app/src/commonTest/java/org/tasks/makers/TaskMaker.kt @@ -1,6 +1,5 @@ package org.tasks.makers -import com.google.ical.values.RRule import com.natpryce.makeiteasy.Instantiator import com.natpryce.makeiteasy.Property import com.natpryce.makeiteasy.Property.newProperty @@ -84,8 +83,7 @@ object TaskMaker { task.reminderPeriod = randomReminderPeriod } lookup.valueOf(RECUR, null as String?)?.let { - val rrule = if (it.startsWith("RRULE:")) it else "RRULE:$it" - task.setRecurrence(RRule(rrule), lookup.valueOf(AFTER_COMPLETE, false)) + task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false)) } task.uuid = lookup.valueOf(UUID, NO_UUID) val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime()) diff --git a/app/src/main/assets/licenses.json b/app/src/main/assets/licenses.json index 4b182e50a..24a29088d 100644 --- a/app/src/main/assets/licenses.json +++ b/app/src/main/assets/licenses.json @@ -372,20 +372,6 @@ "url": "https://developer.android.com/topic/libraries/architecture/index.html", "libraryName": "Android Lifecycle ViewModel" }, - { - "artifactId": { - "name": "google-rfc-2445", - "group": "org.scala-saddle", - "version": "+" - }, - "copyrightHolder": "Google Inc.", - "copyrightStatement": "Copyright © Google Inc. All rights reserved.", - "license": "The Apache Software License, Version 2.0", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt", - "normalizedLicense": "apache2", - "url": "http://code.google.com/p/google-rfc-2445/", - "libraryName": "google-rfc-2445" - }, { "artifactId": { "name": "dagger", diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index 8041abe08..f2d46ae10 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -32,7 +32,6 @@ import butterknife.BindView import butterknife.ButterKnife import butterknife.OnClick import com.google.android.material.snackbar.Snackbar -import com.google.ical.values.RRule import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.adapter.TaskAdapter @@ -73,6 +72,7 @@ import org.tasks.locale.Locale import org.tasks.notifications.NotificationManager import org.tasks.preferences.Device import org.tasks.preferences.Preferences +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.sync.SyncAdapters import org.tasks.tags.TagPickerActivity import org.tasks.tasklist.* @@ -849,12 +849,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL task.setDueDateAdjustingHideUntil(oldDueDate) task.completionDate = 0L try { - val rrule = RRule(task.getRecurrenceWithoutFrom()) - val count = rrule.count + val recur = newRecur(task.recurrence!!) + val count = recur.count if (count > 0) { - rrule.count = count + 1 + recur.count = count + 1 } - task.setRecurrence(rrule, task.repeatAfterCompletion()) + task.setRecurrence(recur.toString(), task.repeatAfterCompletion()) } catch (e: ParseException) { Timber.e(e) } diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index 3e8ba2b72..4b822637f 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -6,10 +6,10 @@ import android.os.Parcelable import androidx.annotation.IntDef import androidx.core.os.ParcelCompat import androidx.room.* -import com.google.ical.values.RRule import com.todoroo.andlib.data.Table import com.todoroo.andlib.sql.Field import com.todoroo.andlib.utility.DateUtilities +import net.fortuna.ical4j.model.Recur import org.tasks.Strings import org.tasks.data.Tag import org.tasks.date.DateTimeUtils @@ -212,10 +212,6 @@ class Task : Parcelable { fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion() - fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.sanitizeRRule() - - fun getRecurrenceWithoutFrom(): String? = recurrence.withoutFrom() - fun setDueDateAdjustingHideUntil(newDueDate: Long) { if (dueDate > 0) { if (hideUntil > 0) { @@ -228,17 +224,17 @@ class Task : Parcelable { val isRecurring: Boolean get() = !Strings.isNullOrEmpty(recurrence) - fun setRecurrence(rrule: RRule, afterCompletion: Boolean) { - recurrence = rrule.toIcal() + if (afterCompletion) ";FROM=COMPLETION" else "" + fun setRecurrence(rrule: String, afterCompletion: Boolean) { + recurrence = rrule + if (afterCompletion) ";FROM=COMPLETION" else "" } - fun setRecurrence(rrule: net.fortuna.ical4j.model.property.RRule?) { + fun setRecurrence(rrule: Recur?) { if (rrule == null) { repeatUntil = 0 recurrence = null } else { - repeatUntil = rrule.recur.until?.let { DateTime(it).millis } ?: 0 - recurrence = "RRULE:${rrule.value.sanitizeRRule()}" + if (repeatAfterCompletion()) ";FROM=COMPLETION" else "" + repeatUntil = rrule.until?.let { DateTime(it).millis } ?: 0 + recurrence = rrule.toString() + if (repeatAfterCompletion()) ";FROM=COMPLETION" else "" } } @@ -603,9 +599,9 @@ class Task : Parcelable { } @JvmStatic - fun String?.sanitizeRRule(): String? = this + fun String?.sanitizeRecur(): String? = this ?.replace("BYDAY=;", "") - ?.replace(INVALID_COUNT, "") + ?.replace(INVALID_COUNT, "") // ical4j adds COUNT=-1 if there is an UNTIL value @JvmStatic fun isUuidEmpty(uuid: String?): Boolean { return NO_UUID == uuid || Strings.isNullOrEmpty(uuid) @@ -614,8 +610,5 @@ class Task : Parcelable { fun String?.isRepeatAfterCompletion() = this?.contains("FROM=COMPLETION") ?: false fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "") - - @JvmStatic - fun String?.withoutRRULE(): String? = this?.replace("^RRULE:".toRegex(), "") } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt index 2d1330602..2f6bc27d4 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt @@ -17,14 +17,14 @@ import android.widget.Spinner import android.widget.TextView import butterknife.BindView import butterknife.OnItemSelected -import com.google.ical.values.Frequency -import com.google.ical.values.RRule -import com.google.ical.values.WeekdayNum import dagger.hilt.android.AndroidEntryPoint +import net.fortuna.ical4j.model.Recur +import net.fortuna.ical4j.model.WeekDay import org.tasks.R import org.tasks.analytics.Firebase import org.tasks.dialogs.DialogBuilder import org.tasks.repeats.BasicRecurrenceDialog +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RepeatRuleToString import org.tasks.themes.Theme import org.tasks.time.DateTime @@ -63,9 +63,9 @@ class RepeatControlSet : TaskEditControlFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == REQUEST_RECURRENCE) { if (resultCode == RESULT_OK) { - viewModel.rrule = data + viewModel.recur = data ?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE) - ?.let { RRule(it) } + ?.let { newRecur(it) } refreshDisplayView() } } else { @@ -74,19 +74,22 @@ class RepeatControlSet : TaskEditControlFragment() { } fun onDueDateChanged() { - viewModel.rrule?.let { - if (it.freq == Frequency.MONTHLY && it.byDay.isNotEmpty()) { - val weekdayNum = it.byDay[0] + viewModel.recur?.let { recur -> + if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) { + val weekdayNum = recur.dayList[0] val dateTime = DateTime(this.dueDate) val num: Int val dayOfWeekInMonth = dateTime.dayOfWeekInMonth - num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) { + num = if (weekdayNum.offset == -1 || dayOfWeekInMonth == 5) { if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth } else { dayOfWeekInMonth } - it.byDay = listOf((WeekdayNum(num, dateTime.weekday))) - viewModel.rrule = it + recur.dayList.let { + it.clear() + it.add(WeekDay(dateTime.weekDay, num)) + } + viewModel.recur = recur refreshDisplayView() } } @@ -127,7 +130,7 @@ class RepeatControlSet : TaskEditControlFragment() { override fun onRowClick() { BasicRecurrenceDialog.newBasicRecurrenceDialog( - this, REQUEST_RECURRENCE, viewModel.rrule, dueDate) + this, REQUEST_RECURRENCE, viewModel.recur?.toString(), dueDate) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) } @@ -140,12 +143,12 @@ class RepeatControlSet : TaskEditControlFragment() { override fun controlId() = TAG private fun refreshDisplayView() { - viewModel.rrule.let { + viewModel.recur.let { if (it == null) { displayView.text = null repeatTypeContainer.visibility = View.GONE } else { - displayView.text = repeatRuleToString.toString(it.toIcal()) + displayView.text = repeatRuleToString.toString(it) repeatTypeContainer.visibility = View.VISIBLE } } diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt index d32ab4258..8984f5b0f 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt @@ -5,20 +5,19 @@ */ package com.todoroo.astrid.repeats -import com.google.ical.iter.RecurrenceIteratorFactory -import com.google.ical.values.* import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task.Companion.createDueDate -import com.todoroo.astrid.data.Task.Companion.withoutFrom import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.service.TaskCompleter +import net.fortuna.ical4j.model.Date +import net.fortuna.ical4j.model.Recur +import net.fortuna.ical4j.model.WeekDay import org.tasks.LocalBroadcastManager -import org.tasks.date.DateTimeUtils.newDate import org.tasks.date.DateTimeUtils.newDateTime -import org.tasks.date.DateTimeUtils.newDateUtc +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.time.DateTime import timber.log.Timber import java.text.ParseException @@ -33,52 +32,53 @@ class RepeatTaskHelper @Inject constructor( private val taskCompleter: TaskCompleter, ) { suspend fun handleRepeat(task: Task) { - val recurrence = task.sanitizedRecurrence() + val recurrence = task.recurrence + if (recurrence.isNullOrBlank()) { + return + } val repeatAfterCompletion = task.repeatAfterCompletion() - if (!recurrence.isNullOrBlank()) { - val newDueDate: Long - val rrule: RRule - try { - rrule = initRRule(task.recurrence) - newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion) - if (newDueDate == -1L) { - return - } - } catch (e: ParseException) { - Timber.e(e) - return - } - val oldDueDate = task.dueDate - val repeatUntil = task.repeatUntil - if (repeatFinished(newDueDate, repeatUntil)) { + val newDueDate: Long + val rrule: Recur + try { + rrule = initRRule(recurrence) + newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion) + if (newDueDate == -1L) { return } - val count = rrule.count - if (count == 1) { - return - } - if (count > 1) { - rrule.count = count - 1 - task.setRecurrence(rrule, repeatAfterCompletion) - } - task.reminderLast = 0L - task.reminderSnooze = 0L - task.completionDate = 0L - task.setDueDateAdjustingHideUntil(newDueDate) - gcalHelper.rescheduleRepeatingTask(task) - taskDao.save(task) - val previousDueDate = - oldDueDate - .takeIf { it > 0 } - ?: newDueDate - (computeNextDueDate(task, recurrence, repeatAfterCompletion) - newDueDate) - alarmService.rescheduleAlarms(task.id, previousDueDate, newDueDate) - taskCompleter.completeChildren(task.id, 0L) - localBroadcastManager.broadcastRepeat(task.id, previousDueDate, newDueDate) + } catch (e: ParseException) { + Timber.e(e) + return + } + val oldDueDate = task.dueDate + val repeatUntil = task.repeatUntil + if (repeatFinished(newDueDate, repeatUntil)) { + return + } + val count = rrule.count + if (count == 1) { + return } + if (count > 1) { + rrule.count = count - 1 + task.setRecurrence(rrule.toString(), repeatAfterCompletion) + } + task.reminderLast = 0L + task.reminderSnooze = 0L + task.completionDate = 0L + task.setDueDateAdjustingHideUntil(newDueDate) + gcalHelper.rescheduleRepeatingTask(task) + taskDao.save(task) + val previousDueDate = + oldDueDate + .takeIf { it > 0 } + ?: newDueDate - (computeNextDueDate(task, recurrence, repeatAfterCompletion) - newDueDate) + alarmService.rescheduleAlarms(task.id, previousDueDate, newDueDate) + taskCompleter.completeChildren(task.id, 0L) + localBroadcastManager.broadcastRepeat(task.id, previousDueDate, newDueDate) } companion object { - private val weekdayCompare = Comparator { object1: WeekdayNum, object2: WeekdayNum -> object1.wday.javaDayNum - object2.wday.javaDayNum } + private val weekdayCompare = Comparator { object1: WeekDay, object2: WeekDay -> WeekDay.getCalendarDay(object1) - WeekDay.getCalendarDay(object2) } private fun repeatFinished(newDueDate: Long, repeatUntil: Long): Boolean { return (repeatUntil > 0 && newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay())) @@ -86,17 +86,17 @@ class RepeatTaskHelper @Inject constructor( /** Compute next due date */ @Throws(ParseException::class) - fun computeNextDueDate(task: Task, recurrence: String?, repeatAfterCompletion: Boolean): Long { + fun computeNextDueDate(task: Task, recurrence: String, repeatAfterCompletion: Boolean): Long { val rrule = initRRule(recurrence) // initialize startDateAsDV - val original = setUpStartDate(task, repeatAfterCompletion, rrule.freq) + val original = setUpStartDate(task, repeatAfterCompletion, rrule.frequency) val startDateAsDV = setUpStartDateAsDV(task, original) - return if (rrule.freq == Frequency.HOURLY || rrule.freq == Frequency.MINUTELY) { + return if (rrule.frequency == Recur.Frequency.HOURLY || rrule.frequency == Recur.Frequency.MINUTELY) { handleSubdayRepeat(original, rrule) - } else if (rrule.freq == Frequency.WEEKLY && rrule.byDay.size > 0 && repeatAfterCompletion) { + } else if (rrule.frequency == Recur.Frequency.WEEKLY && rrule.dayList.isNotEmpty() && repeatAfterCompletion) { handleWeeklyRepeatAfterComplete(rrule, original, task.hasDueTime()) - } else if (rrule.freq == Frequency.MONTHLY && rrule.byDay.isEmpty()) { + } else if (rrule.frequency == Recur.Frequency.MONTHLY && rrule.dayList.isEmpty()) { handleMonthlyRepeat(original, startDateAsDV, task.hasDueTime(), rrule) } else { invokeRecurrence(rrule, original, startDateAsDV) @@ -104,16 +104,16 @@ class RepeatTaskHelper @Inject constructor( } private fun handleWeeklyRepeatAfterComplete( - rrule: RRule, original: DateTime, hasDueTime: Boolean): Long { - val byDay = rrule.byDay + recur: Recur, original: DateTime, hasDueTime: Boolean): Long { + val byDay = recur.dayList var newDate = original.millis - newDate += DateUtilities.ONE_WEEK * (rrule.interval - 1) + newDate += DateUtilities.ONE_WEEK * (recur.interval - 1) var date = DateTime(newDate) Collections.sort(byDay, weekdayCompare) val next = findNextWeekday(byDay, date) do { date = date.plusDays(1) - } while (date.dayOfWeek != next.wday.javaDayNum) + } while (date.dayOfWeek != WeekDay.getCalendarDay(next)) val time = date.millis return if (hasDueTime) { createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time) @@ -123,9 +123,9 @@ class RepeatTaskHelper @Inject constructor( } private fun handleMonthlyRepeat( - original: DateTime, startDateAsDV: DateValue, hasDueTime: Boolean, rrule: RRule): Long { + original: DateTime, startDateAsDV: Date, hasDueTime: Boolean, recur: Recur): Long { return if (original.isLastDayOfMonth) { - val interval = rrule.interval + val interval = recur.interval val newDateTime = original.plusMonths(interval) val time = newDateTime.withDayOfMonth(newDateTime.numberOfDaysInMonth).millis if (hasDueTime) { @@ -134,90 +134,60 @@ class RepeatTaskHelper @Inject constructor( createDueDate(Task.URGENCY_SPECIFIC_DAY, time) } } else { - invokeRecurrence(rrule, original, startDateAsDV) + invokeRecurrence(recur, original, startDateAsDV) } } - private fun findNextWeekday(byDay: List, date: DateTime): WeekdayNum { + private fun findNextWeekday(byDay: List, date: DateTime): WeekDay { val next = byDay[0] for (weekday in byDay) { - if (weekday.wday.javaDayNum > date.dayOfWeek) { + if (WeekDay.getCalendarDay(weekday) > date.dayOfWeek) { return weekday } } return next } - private fun invokeRecurrence(rrule: RRule, original: DateTime, startDateAsDV: DateValue): Long { - var newDueDate: Long = -1 - val iterator = RecurrenceIteratorFactory.createRecurrenceIterator( - rrule, startDateAsDV, TimeZone.getDefault()) - var nextDate: DateValue - for (i in 0..9) { // ten tries then we give up - if (!iterator.hasNext()) { - return -1 - } - nextDate = iterator.next() - if (nextDate.compareTo(startDateAsDV) == 0) { - continue - } - newDueDate = buildNewDueDate(original, nextDate) - - // detect if we finished - if (newDueDate > original.millis) { - break - } - } - return newDueDate + private fun invokeRecurrence(recur: Recur, original: DateTime, startDateAsDV: Date): Long { + val nextDate = recur.getNextDate(startDateAsDV, startDateAsDV) + return buildNewDueDate(original, nextDate) } /** Compute long due date from DateValue */ - private fun buildNewDueDate(original: DateTime, nextDate: DateValue): Long { + private fun buildNewDueDate(original: DateTime, nextDate: Date): Long { val newDueDate: Long - if (nextDate is DateTimeValueImpl) { - var date = newDateUtc( - nextDate.year(), - nextDate.month(), - nextDate.day(), - nextDate.hour(), - nextDate.minute(), - nextDate.second()) - .toLocal() + if (nextDate is net.fortuna.ical4j.model.DateTime) { + var date = DateTime.from(nextDate) // time may be inaccurate due to DST, force time to be same date = date.withHourOfDay(original.hourOfDay).withMinuteOfHour(original.minuteOfHour) newDueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.millis) } else { newDueDate = createDueDate( Task.URGENCY_SPECIFIC_DAY, - newDate(nextDate.year(), nextDate.month(), nextDate.day()).millis) + DateTime.from(nextDate).millis) } return newDueDate } /** Initialize RRule instance */ @Throws(ParseException::class) - private fun initRRule(recurrence: String?): RRule { - val rrule = RRule(recurrence - ?.let { if (it.startsWith("RRULE:")) it else "RRULE:$it" } - ?.withoutFrom()) + private fun initRRule(recurrence: String): Recur { + val rrule = newRecur(recurrence) - if (rrule.count < 0) { - rrule.count = 0 - } // handle the iCalendar "byDay" field differently depending on if // we are weekly or otherwise - if (rrule.freq != Frequency.WEEKLY && rrule.freq != Frequency.MONTHLY) { - rrule.byDay = emptyList() + if (rrule.frequency != Recur.Frequency.WEEKLY && rrule.frequency != Recur.Frequency.MONTHLY) { + rrule.dayList.clear() } return rrule } /** Set up repeat start date */ private fun setUpStartDate( - task: Task, repeatAfterCompletion: Boolean, frequency: Frequency): DateTime { + task: Task, repeatAfterCompletion: Boolean, frequency: Recur.Frequency): DateTime { return if (repeatAfterCompletion) { var startDate = if (task.isCompleted) newDateTime(task.completionDate) else newDateTime() - if (task.hasDueTime() && frequency != Frequency.HOURLY && frequency != Frequency.MINUTELY) { + if (task.hasDueTime() && frequency != Recur.Frequency.HOURLY && frequency != Recur.Frequency.MINUTELY) { val dueDate = newDateTime(task.dueDate) startDate = startDate .withHourOfDay(dueDate.hourOfDay) @@ -230,29 +200,22 @@ class RepeatTaskHelper @Inject constructor( } } - private fun setUpStartDateAsDV(task: Task, startDate: DateTime): DateValue { + private fun setUpStartDateAsDV(task: Task, startDate: DateTime): Date { return if (task.hasDueTime()) { - DateTimeValueImpl( - startDate.year, - startDate.monthOfYear, - startDate.dayOfMonth, - startDate.hourOfDay, - startDate.minuteOfHour, - startDate.secondOfMinute) + startDate.toDateTime() } else { - DateValueImpl( - startDate.year, startDate.monthOfYear, startDate.dayOfMonth) + startDate.toDate() } } - private fun handleSubdayRepeat(startDate: DateTime, rrule: RRule): Long { - val millis: Long = when (rrule.freq) { - Frequency.HOURLY -> DateUtilities.ONE_HOUR - Frequency.MINUTELY -> DateUtilities.ONE_MINUTE + private fun handleSubdayRepeat(startDate: DateTime, recur: Recur): Long { + val millis: Long = when (recur.frequency) { + Recur.Frequency.HOURLY -> DateUtilities.ONE_HOUR + Recur.Frequency.MINUTELY -> DateUtilities.ONE_MINUTE else -> throw RuntimeException( - "Error handing subday repeat: " + rrule.freq) // $NON-NLS-1$ + "Error handing subday repeat: " + recur.frequency) // $NON-NLS-1$ } - val newDueDate = startDate.millis + millis * rrule.interval + val newDueDate = startDate.millis + millis * recur.interval return createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDueDate) } } diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt index 589bcedb0..a6f6edafe 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt @@ -1,6 +1,5 @@ package com.todoroo.astrid.service -import com.google.ical.values.RRule import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.Filter @@ -105,7 +104,7 @@ class TaskCreator @Inject constructor( ?.takeIf { it.isNotBlank() } ?.let { task.setRecurrence( - RRule(it), + it, preferences.getIntegerFromString(R.string.p_default_recurrence_from, 0) == 1) } preferences.getStringValue(R.string.p_default_location) diff --git a/app/src/main/java/com/todoroo/astrid/utility/TitleParser.kt b/app/src/main/java/com/todoroo/astrid/utility/TitleParser.kt index e7a25dda1..ee59118f8 100644 --- a/app/src/main/java/com/todoroo/astrid/utility/TitleParser.kt +++ b/app/src/main/java/com/todoroo/astrid/utility/TitleParser.kt @@ -5,14 +5,14 @@ */ package com.todoroo.astrid.utility -import com.google.ical.values.Frequency -import com.google.ical.values.RRule import com.mdimension.jchronic.AstridChronic import com.mdimension.jchronic.Chronic import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task.Companion.createDueDate +import net.fortuna.ical4j.model.Recur.Frequency import org.tasks.Strings.isNullOrEmpty import org.tasks.data.TagDataDao +import org.tasks.repeats.RecurrenceUtils.newRecur import timber.log.Timber import java.util.* import java.util.regex.Matcher @@ -373,10 +373,10 @@ object TitleParser { val m = pattern.matcher(inputText) if (m.find()) { val rtime = repeatTimes[repeatTime] - val rrule = RRule() - rrule.freq = rtime - rrule.interval = findInterval(inputText) - task.recurrence = rrule.toIcal() + val recur = newRecur() + recur.setFrequency(rtime!!.name) + recur.interval = findInterval(inputText) + task.recurrence = recur.toString() return } } @@ -385,11 +385,10 @@ object TitleParser { val m = pattern.matcher(inputText) if (m.find()) { val rtime = repeatTimesIntervalOne[repeatTimeIntervalOne] - val rrule = RRule() - rrule.freq = rtime - rrule.interval = 1 - val thing = rrule.toIcal() - task.recurrence = thing + val recur = newRecur() + recur.setFrequency(rtime!!.name) + recur.interval = 1 + task.recurrence = recur.toString() return } } diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt index 0a1ce00c1..0c068da85 100644 --- a/app/src/main/java/org/tasks/caldav/iCalendar.kt +++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt @@ -9,8 +9,6 @@ import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY_TIME import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME -import com.todoroo.astrid.data.Task.Companion.sanitizeRRule -import com.todoroo.astrid.data.Task.Companion.withoutRRULE import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.service.TaskCreator import net.fortuna.ical4j.model.Date @@ -28,6 +26,8 @@ import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.jobs.WorkManager import org.tasks.location.GeofenceApi import org.tasks.preferences.Preferences +import org.tasks.repeats.RecurrenceUtils.newRRule +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.time.DateTime.UTC import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfMinute @@ -205,18 +205,8 @@ class iCalendar @Inject constructor( } @JvmStatic - fun getLocal(property: DateProperty): Long { - val dateTime: org.tasks.time.DateTime? = if (property.date is DateTime) { - val dt = property.date as DateTime - org.tasks.time.DateTime( - dt.time, - dt.timeZone ?: if (dt.isUtc) UTC else TimeZone.getDefault() - ) - } else { - org.tasks.time.DateTime.from(property.date) - } - return dateTime?.toLocal()?.millis ?: 0 - } + fun getLocal(property: DateProperty): Long = + org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0 fun fromVtodo(vtodo: String): Task? { try { @@ -295,7 +285,7 @@ class iCalendar @Inject constructor( in 6..9 -> com.todoroo.astrid.data.Task.Priority.LOW else -> com.todoroo.astrid.data.Task.Priority.NONE } - setRecurrence(remote.rRule) + setRecurrence(remote.rRule?.recur) remote.due.apply(this) remote.dtStart.apply(this) } @@ -333,12 +323,14 @@ class iCalendar @Inject constructor( } rRule = if (task.isRecurring) { try { - val rrule = RRule(task.getRecurrenceWithoutFrom().withoutRRULE()) + val recur = newRecur(task.recurrence!!) val repeatUntil = task.repeatUntil - rrule - .recur.until = if (repeatUntil > 0) DateTime(newDateTime(repeatUntil).toUTC().millis) else null - val sanitized: String = rrule.value.sanitizeRRule()!! // ical4j adds COUNT=-1 if there is an UNTIL value - RRule(sanitized) + recur.until = if (repeatUntil > 0) { + DateTime(newDateTime(repeatUntil).toUTC().millis) + } else { + null + } + newRRule(recur.toString()) } catch (e: ParseException) { Timber.e(e) null diff --git a/app/src/main/java/org/tasks/preferences/fragments/TaskDefaults.kt b/app/src/main/java/org/tasks/preferences/fragments/TaskDefaults.kt index bfd0fd2d4..774acd85e 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/TaskDefaults.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/TaskDefaults.kt @@ -6,7 +6,6 @@ import android.os.Bundle import android.view.View import androidx.lifecycle.lifecycleScope import androidx.preference.Preference -import com.google.ical.values.RRule import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.GtasksFilter @@ -78,10 +77,9 @@ class TaskDefaults : InjectingPreferenceFragment() { findPreference(R.string.p_default_recurrence) .setOnPreferenceClickListener { - val rrule: RRule? = preferences + val rrule = preferences .getStringValue(R.string.p_default_recurrence) ?.takeIf { it.isNotBlank() } - ?.let { RRule(it) } BasicRecurrenceDialog .newBasicRecurrenceDialog(this, REQUEST_RECURRENCE, rrule, -1) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) diff --git a/app/src/main/java/org/tasks/repeats/BasicRecurrenceDialog.java b/app/src/main/java/org/tasks/repeats/BasicRecurrenceDialog.java index c4c4690fd..7dfb65337 100644 --- a/app/src/main/java/org/tasks/repeats/BasicRecurrenceDialog.java +++ b/app/src/main/java/org/tasks/repeats/BasicRecurrenceDialog.java @@ -2,14 +2,15 @@ package org.tasks.repeats; import static android.app.Activity.RESULT_OK; import static com.google.common.collect.Lists.newArrayList; -import static com.google.ical.values.Frequency.DAILY; -import static com.google.ical.values.Frequency.HOURLY; -import static com.google.ical.values.Frequency.MINUTELY; -import static com.google.ical.values.Frequency.MONTHLY; -import static com.google.ical.values.Frequency.WEEKLY; -import static com.google.ical.values.Frequency.YEARLY; +import static net.fortuna.ical4j.model.Recur.Frequency.DAILY; +import static net.fortuna.ical4j.model.Recur.Frequency.HOURLY; +import static net.fortuna.ical4j.model.Recur.Frequency.MINUTELY; +import static net.fortuna.ical4j.model.Recur.Frequency.MONTHLY; +import static net.fortuna.ical4j.model.Recur.Frequency.WEEKLY; +import static net.fortuna.ical4j.model.Recur.Frequency.YEARLY; import static org.tasks.Strings.isNullOrEmpty; import static org.tasks.repeats.CustomRecurrenceDialog.newCustomRecurrenceDialog; +import static org.tasks.repeats.RecurrenceUtils.newRecur; import static org.tasks.time.DateTimeUtils.currentTimeMillis; import android.app.Activity; @@ -20,11 +21,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; -import com.google.ical.values.Frequency; -import com.google.ical.values.RRule; import dagger.hilt.android.AndroidEntryPoint; import java.util.List; import javax.inject.Inject; +import net.fortuna.ical4j.model.Recur; +import net.fortuna.ical4j.model.Recur.Frequency; import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.ui.SingleCheckedArrayAdapter; @@ -42,12 +43,12 @@ public class BasicRecurrenceDialog extends DialogFragment { @Inject RepeatRuleToString repeatRuleToString; public static BasicRecurrenceDialog newBasicRecurrenceDialog( - Fragment target, int rc, RRule rrule, long dueDate) { + Fragment target, int rc, String rrule, long dueDate) { BasicRecurrenceDialog dialog = new BasicRecurrenceDialog(); dialog.setTargetFragment(target, rc); Bundle arguments = new Bundle(); if (rrule != null) { - arguments.putString(EXTRA_RRULE, rrule.toIcal()); + arguments.putString(EXTRA_RRULE, rrule); } arguments.putLong(EXTRA_DATE, dueDate); dialog.setArguments(arguments); @@ -60,15 +61,14 @@ public class BasicRecurrenceDialog extends DialogFragment { Bundle arguments = getArguments(); long dueDate = arguments.getLong(EXTRA_DATE, currentTimeMillis()); String rule = arguments.getString(EXTRA_RRULE); - RRule parsed = null; + Recur rrule = null; try { if (!isNullOrEmpty(rule)) { - parsed = new RRule(rule); + rrule = newRecur(rule); } } catch (Exception e) { Timber.e(e); } - RRule rrule = parsed; boolean customPicked = isCustomValue(rrule); List repeatOptions = @@ -79,7 +79,7 @@ public class BasicRecurrenceDialog extends DialogFragment { if (customPicked) { adapter.insert(repeatRuleToString.toString(rule), 0); } else if (rrule != null) { - switch (rrule.getFreq()) { + switch (rrule.getFrequency()) { case DAILY: selected = 1; break; @@ -110,37 +110,37 @@ public class BasicRecurrenceDialog extends DialogFragment { } i--; } - RRule result; + Recur result; if (i == 0) { result = null; } else if (i == 5) { newCustomRecurrenceDialog( - getTargetFragment(), getTargetRequestCode(), rrule, dueDate) + getTargetFragment(), getTargetRequestCode(), rule, dueDate) .show(getParentFragmentManager(), FRAG_TAG_CUSTOM_RECURRENCE); dialogInterface.dismiss(); return; } else { - result = new RRule(); + result = newRecur(); result.setInterval(1); switch (i) { case 1: - result.setFreq(DAILY); + result.setFrequency(DAILY.name()); break; case 2: - result.setFreq(WEEKLY); + result.setFrequency(WEEKLY.name()); break; case 3: - result.setFreq(MONTHLY); + result.setFrequency(MONTHLY.name()); break; case 4: - result.setFreq(YEARLY); + result.setFrequency(YEARLY.name()); break; } } Intent intent = new Intent(); - intent.putExtra(EXTRA_RRULE, result == null ? null : result.toIcal()); + intent.putExtra(EXTRA_RRULE, result == null ? null : result.toString()); getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent); dialogInterface.dismiss(); }) @@ -148,16 +148,16 @@ public class BasicRecurrenceDialog extends DialogFragment { .show(); } - private boolean isCustomValue(RRule rrule) { + private boolean isCustomValue(Recur rrule) { if (rrule == null) { return false; } - Frequency frequency = rrule.getFreq(); - return (frequency == WEEKLY || frequency == MONTHLY) && !rrule.getByDay().isEmpty() + Frequency frequency = rrule.getFrequency(); + return (frequency == WEEKLY || frequency == MONTHLY) && !rrule.getDayList().isEmpty() || frequency == HOURLY || frequency == MINUTELY || rrule.getUntil() != null - || rrule.getInterval() != 1 - || rrule.getCount() != 0; + || rrule.getInterval() > 1 + || rrule.getCount() > 0; } } diff --git a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java index f049de7ed..9e985c910 100644 --- a/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java +++ b/app/src/main/java/org/tasks/repeats/CustomRecurrenceDialog.java @@ -2,16 +2,17 @@ package org.tasks.repeats; import static android.app.Activity.RESULT_OK; import static com.google.common.collect.Lists.newArrayList; -import static com.google.ical.values.Frequency.DAILY; -import static com.google.ical.values.Frequency.HOURLY; -import static com.google.ical.values.Frequency.MINUTELY; -import static com.google.ical.values.Frequency.MONTHLY; -import static com.google.ical.values.Frequency.WEEKLY; -import static com.google.ical.values.Frequency.YEARLY; import static java.util.Arrays.asList; +import static net.fortuna.ical4j.model.Recur.Frequency.DAILY; +import static net.fortuna.ical4j.model.Recur.Frequency.HOURLY; +import static net.fortuna.ical4j.model.Recur.Frequency.MINUTELY; +import static net.fortuna.ical4j.model.Recur.Frequency.MONTHLY; +import static net.fortuna.ical4j.model.Recur.Frequency.WEEKLY; +import static net.fortuna.ical4j.model.Recur.Frequency.YEARLY; import static org.tasks.Strings.isNullOrEmpty; import static org.tasks.dialogs.MyDatePickerDialog.newDatePicker; import static org.tasks.repeats.BasicRecurrenceDialog.EXTRA_RRULE; +import static org.tasks.repeats.RecurrenceUtils.newRecur; import static org.tasks.time.DateTimeUtils.currentTimeMillis; import android.app.Activity; @@ -46,19 +47,17 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnItemSelected; import butterknife.OnTextChanged; -import com.google.ical.values.Frequency; -import com.google.ical.values.RRule; -import com.google.ical.values.Weekday; -import com.google.ical.values.WeekdayNum; import com.todoroo.andlib.utility.DateUtilities; import dagger.hilt.android.AndroidEntryPoint; import java.text.DateFormatSymbols; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; import java.util.List; import javax.inject.Inject; +import net.fortuna.ical4j.model.Recur; +import net.fortuna.ical4j.model.Recur.Frequency; +import net.fortuna.ical4j.model.WeekDay; import org.tasks.R; import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.MyDatePickerDialog; @@ -140,16 +139,16 @@ public class CustomRecurrenceDialog extends DialogFragment { private ArrayAdapter repeatUntilAdapter; private ToggleButton[] weekButtons; - private RRule rrule; + private Recur rrule; private long dueDate; public static CustomRecurrenceDialog newCustomRecurrenceDialog( - Fragment target, int rc, RRule rrule, long dueDate) { + Fragment target, int rc, String rrule, long dueDate) { CustomRecurrenceDialog dialog = new CustomRecurrenceDialog(); dialog.setTargetFragment(target, rc); Bundle arguments = new Bundle(); if (rrule != null) { - arguments.putString(EXTRA_RRULE, rrule.toIcal()); + arguments.putString(EXTRA_RRULE, rrule); } arguments.putLong(EXTRA_DATE, dueDate); dialog.setArguments(arguments); @@ -170,15 +169,15 @@ public class CustomRecurrenceDialog extends DialogFragment { : savedInstanceState.getString(EXTRA_RRULE); try { if (!isNullOrEmpty(rule)) { - rrule = new RRule(rule); + rrule = newRecur(rule); } } catch (Exception e) { Timber.e(e); } if (rrule == null) { - rrule = new RRule(); + rrule = newRecur(); rrule.setInterval(1); - rrule.setFreq(WEEKLY); + rrule.setFrequency(WEEKLY.name()); } DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale()); @@ -197,7 +196,7 @@ public class CustomRecurrenceDialog extends DialogFragment { repeatMonthlyDayOfLastWeek.setVisibility(View.VISIBLE); String last = getString(R.string.repeat_monthly_last_week); String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, last, today); - repeatMonthlyDayOfLastWeek.setTag(new WeekdayNum(-1, calendarDayToWeekday(dueDayOfWeek))); + repeatMonthlyDayOfLastWeek.setTag(new WeekDay(calendarDayToWeekday(dueDayOfWeek), -1)); repeatMonthlyDayOfLastWeek.setText(text); } else { repeatMonthlyDayOfLastWeek.setVisibility(View.GONE); @@ -216,18 +215,18 @@ public class CustomRecurrenceDialog extends DialogFragment { String nth = getString(resources[dayOfWeekInMonth - 1]); String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, nth, today); repeatMonthlyDayOfNthWeek.setTag( - new WeekdayNum(dayOfWeekInMonth, calendarDayToWeekday(dueDayOfWeek))); + new WeekDay(calendarDayToWeekday(dueDayOfWeek), dayOfWeekInMonth)); repeatMonthlyDayOfNthWeek.setText(text); } else { repeatMonthlyDayOfNthWeek.setVisibility(View.GONE); } - if (rrule.getFreq() == MONTHLY) { - if (rrule.getByDay().size() == 1) { - WeekdayNum weekdayNum = rrule.getByDay().get(0); - if (weekdayNum.num == -1) { + if (rrule.getFrequency() == MONTHLY) { + if (rrule.getDayList().size() == 1) { + WeekDay weekday = rrule.getDayList().get(0); + if (weekday.getOffset() == -1) { repeatMonthlyDayOfLastWeek.setChecked(true); - } else if (weekdayNum.num == dayOfWeekInMonth) { + } else if (weekday.getOffset() == dayOfWeekInMonth) { repeatMonthlyDayOfNthWeek.setChecked(true); } } @@ -241,7 +240,7 @@ public class CustomRecurrenceDialog extends DialogFragment { ArrayAdapter.createFromResource(context, R.array.repeat_frequency, R.layout.frequency_item); frequencyAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); frequencySpinner.setAdapter(frequencyAdapter); - frequencySpinner.setSelection(FREQUENCIES.indexOf(rrule.getFreq())); + frequencySpinner.setSelection(FREQUENCIES.indexOf(rrule.getFrequency())); intervalEditText.setText(locale.formatNumber(rrule.getInterval())); @@ -280,7 +279,7 @@ public class CustomRecurrenceDialog extends DialogFragment { Calendar dayOfWeekCalendar = Calendar.getInstance(locale.getLocale()); dayOfWeekCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeekCalendar.getFirstDayOfWeek()); - WeekdayNum todayWeekday = new WeekdayNum(0, new DateTime(dueDate).getWeekday()); + WeekDay todayWeekday = new WeekDay(new DateTime(dueDate).getWeekDay(), 0); ColorStateList colorStateList = new ColorStateList( @@ -323,12 +322,12 @@ public class CustomRecurrenceDialog extends DialogFragment { weekButton.setTextColor(colorStateList); weekButton.setTextOn(text); weekButton.setTextOff(text); - weekButton.setTag(new WeekdayNum(0, calendarDayToWeekday(dayOfWeek))); + weekButton.setTag(new WeekDay(calendarDayToWeekday(dayOfWeek), 0)); if (savedInstanceState == null) { weekButton.setChecked( - rrule.getFreq() != WEEKLY || rrule.getByDay().isEmpty() + rrule.getFrequency() != WEEKLY || rrule.getDayList().isEmpty() ? todayWeekday.equals(weekButton.getTag()) - : rrule.getByDay().contains(weekButton.getTag())); + : rrule.getDayList().contains(weekButton.getTag())); } dayOfWeekCalendar.add(Calendar.DATE, 1); } @@ -344,51 +343,54 @@ public class CustomRecurrenceDialog extends DialogFragment { } private void onRuleSelected(DialogInterface dialogInterface, int which) { - if (rrule.getFreq() == WEEKLY) { - List checked = new ArrayList<>(); + if (rrule.getFrequency() == WEEKLY) { + List checked = new ArrayList<>(); for (ToggleButton weekButton : weekButtons) { if (weekButton.isChecked()) { - checked.add((WeekdayNum) weekButton.getTag()); + checked.add((WeekDay) weekButton.getTag()); } } - rrule.setByDay(checked); - } else if (rrule.getFreq() == MONTHLY) { + rrule.getDayList().clear(); + rrule.getDayList().addAll(checked); + } else if (rrule.getFrequency() == MONTHLY) { switch (monthGroup.getCheckedRadioButtonId()) { case R.id.repeat_monthly_same_day: - rrule.setByDay(Collections.emptyList()); + rrule.getDayList().clear(); break; case R.id.repeat_monthly_day_of_nth_week: - rrule.setByDay(newArrayList((WeekdayNum) repeatMonthlyDayOfNthWeek.getTag())); + rrule.getDayList().clear(); + rrule.getDayList().addAll(newArrayList((WeekDay) repeatMonthlyDayOfNthWeek.getTag())); break; case R.id.repeat_monthly_day_of_last_week: - rrule.setByDay(newArrayList((WeekdayNum) repeatMonthlyDayOfLastWeek.getTag())); + rrule.getDayList().clear(); + rrule.getDayList().addAll(newArrayList((WeekDay) repeatMonthlyDayOfLastWeek.getTag())); break; } } else { - rrule.setByDay(Collections.emptyList()); + rrule.getDayList().clear(); } Intent intent = new Intent(); - intent.putExtra(EXTRA_RRULE, rrule.toIcal()); + intent.putExtra(EXTRA_RRULE, rrule.toString()); getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent); dismiss(); } - private Weekday calendarDayToWeekday(int calendarDay) { + private WeekDay calendarDayToWeekday(int calendarDay) { switch (calendarDay) { case Calendar.SUNDAY: - return Weekday.SU; + return WeekDay.SU; case Calendar.MONDAY: - return Weekday.MO; + return WeekDay.MO; case Calendar.TUESDAY: - return Weekday.TU; + return WeekDay.TU; case Calendar.WEDNESDAY: - return Weekday.WE; + return WeekDay.WE; case Calendar.THURSDAY: - return Weekday.TH; + return WeekDay.TH; case Calendar.FRIDAY: - return Weekday.FR; + return WeekDay.FR; case Calendar.SATURDAY: - return Weekday.SA; + return WeekDay.SA; } throw new RuntimeException("Invalid calendar day: " + calendarDay); } @@ -397,7 +399,7 @@ public class CustomRecurrenceDialog extends DialogFragment { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putString(EXTRA_RRULE, rrule.toIcal()); + outState.putString(EXTRA_RRULE, rrule.toString()); } private void setInterval(int interval, boolean updateEditText) { @@ -428,7 +430,7 @@ public class CustomRecurrenceDialog extends DialogFragment { } private int getFrequencyPlural() { - switch (rrule.getFreq()) { + switch (rrule.getFrequency()) { case MINUTELY: return R.plurals.repeat_minutes; case HOURLY: @@ -442,7 +444,7 @@ public class CustomRecurrenceDialog extends DialogFragment { case YEARLY: return R.plurals.repeat_years; default: - throw new RuntimeException("Invalid frequency: " + rrule.getFreq()); + throw new RuntimeException("Invalid frequency: " + rrule.getFrequency()); } } @@ -467,7 +469,7 @@ public class CustomRecurrenceDialog extends DialogFragment { @OnItemSelected(R.id.frequency) public void onFrequencyChanged(int position) { Frequency frequency = FREQUENCIES.get(position); - rrule.setFreq(frequency); + rrule.setFrequency(frequency.name()); int weekVisibility = frequency == WEEKLY ? View.VISIBLE : View.GONE; weekGroup1.setVisibility(weekVisibility); if (weekGroup2 != null) { @@ -545,7 +547,7 @@ public class CustomRecurrenceDialog extends DialogFragment { if (requestCode == REQUEST_PICK_DATE) { if (resultCode == RESULT_OK) { rrule.setUntil( - new DateTime(data.getLongExtra(MyDatePickerDialog.EXTRA_TIMESTAMP, 0L)).toDateValue()); + new DateTime(data.getLongExtra(MyDatePickerDialog.EXTRA_TIMESTAMP, 0L)).toDate()); rrule.setCount(0); } updateRepeatUntilOptions(); diff --git a/app/src/main/java/org/tasks/repeats/RecurrenceUtils.kt b/app/src/main/java/org/tasks/repeats/RecurrenceUtils.kt index e069d3ede..468d56060 100644 --- a/app/src/main/java/org/tasks/repeats/RecurrenceUtils.kt +++ b/app/src/main/java/org/tasks/repeats/RecurrenceUtils.kt @@ -1,5 +1,6 @@ package org.tasks.repeats +import com.todoroo.astrid.data.Task.Companion.sanitizeRecur import com.todoroo.astrid.data.Task.Companion.withoutFrom import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.property.RRule @@ -14,6 +15,6 @@ object RecurrenceUtils { fun newRecur(rrule: String): Recur = newRRule(rrule).recur fun newRRule(rrule: String): RRule = - RRule(rrule.replace(LEGACY_RRULE_PREFIX, "").withoutFrom()) + RRule(rrule.replace(LEGACY_RRULE_PREFIX, "").withoutFrom().sanitizeRecur()) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/repeats/RepeatRuleToString.kt b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.kt index 4df265120..f0d000c9c 100644 --- a/app/src/main/java/org/tasks/repeats/RepeatRuleToString.kt +++ b/app/src/main/java/org/tasks/repeats/RepeatRuleToString.kt @@ -2,17 +2,16 @@ package org.tasks.repeats import android.content.Context import com.todoroo.andlib.utility.DateUtilities -import com.todoroo.astrid.data.Task.Companion.withoutRRULE import dagger.hilt.android.qualifiers.ApplicationContext import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur.Frequency import net.fortuna.ical4j.model.Recur.Frequency.* import net.fortuna.ical4j.model.WeekDay.Day import net.fortuna.ical4j.model.WeekDay.Day.* -import net.fortuna.ical4j.model.property.RRule import org.tasks.R import org.tasks.analytics.Firebase import org.tasks.locale.Locale +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.time.DateTime import java.text.DateFormatSymbols import java.text.ParseException @@ -27,14 +26,13 @@ class RepeatRuleToString @Inject constructor( private val weekdays = listOf(*Day.values()) fun toString(rrule: String?): String? = try { - toString(RRule(rrule.withoutRRULE())) + rrule?.let { toString(newRecur(it)) } } catch (e: ParseException) { firebase.reportException(e) null } - fun toString(r: RRule): String { - val rrule = r.recur + fun toString(rrule: Recur): String { val interval = rrule.interval val frequency = rrule.frequency val repeatUntil = if (rrule.until == null) null else DateTime.from(rrule.until) diff --git a/app/src/main/java/org/tasks/time/DateTime.java b/app/src/main/java/org/tasks/time/DateTime.java index 9f94f9c12..397b43851 100644 --- a/app/src/main/java/org/tasks/time/DateTime.java +++ b/app/src/main/java/org/tasks/time/DateTime.java @@ -10,10 +10,6 @@ import static java.util.Calendar.TUESDAY; import static java.util.Calendar.WEDNESDAY; import static org.tasks.time.DateTimeUtils.currentTimeMillis; -import com.google.ical.values.DateTimeValue; -import com.google.ical.values.DateValue; -import com.google.ical.values.DateValueImpl; -import com.google.ical.values.Weekday; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; @@ -23,6 +19,7 @@ import java.util.GregorianCalendar; import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import net.fortuna.ical4j.model.WeekDay; import org.tasks.locale.Locale; public class DateTime { @@ -86,19 +83,24 @@ public class DateTime { } public static DateTime from(Date date) { + if (date == null) { + return new DateTime(0); + } DateTime dateTime = new DateTime(date.getTime()); return dateTime.minusMillis(dateTime.getOffset()); } - public static DateTime from(DateValue dateValue) { - if (dateValue == null) { - return new DateTime(0); - } - if (dateValue instanceof DateTimeValue) { - DateTimeValue dt = (DateTimeValue) dateValue; - return new DateTime(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()); + public static DateTime from(net.fortuna.ical4j.model.Date date) { + if (date instanceof net.fortuna.ical4j.model.DateTime) { + net.fortuna.ical4j.model.DateTime dt = (net.fortuna.ical4j.model.DateTime) date; + TimeZone tz = dt.getTimeZone(); + return new DateTime( + dt.getTime(), + tz != null ? tz : dt.isUtc() ? UTC : TimeZone.getDefault() + ); + } else { + return from((java.util.Date) date); } - return new DateTime(dateValue.year(), dateValue.month(), dateValue.day()); } public DateTime(Date date) { @@ -358,8 +360,8 @@ public class DateTime { return timestamp == 0 ? null : new net.fortuna.ical4j.model.DateTime(timestamp); } - public DateValue toDateValue() { - return timestamp == 0 ? null : new DateValueImpl(getYear(), getMonthOfYear(), getDayOfMonth()); + public net.fortuna.ical4j.model.Date toDate() { + return timestamp == 0 ? null : new net.fortuna.ical4j.model.Date(timestamp); } public LocalDate toLocalDate() { @@ -382,22 +384,22 @@ public class DateTime { return getCalendar().getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH); } - public Weekday getWeekday() { + public WeekDay getWeekDay() { switch (getCalendar().get(Calendar.DAY_OF_WEEK)) { case SUNDAY: - return Weekday.SU; + return WeekDay.SU; case MONDAY: - return Weekday.MO; + return WeekDay.MO; case TUESDAY: - return Weekday.TU; + return WeekDay.TU; case WEDNESDAY: - return Weekday.WE; + return WeekDay.WE; case THURSDAY: - return Weekday.TH; + return WeekDay.TH; case FRIDAY: - return Weekday.FR; + return WeekDay.FR; case SATURDAY: - return Weekday.SA; + return WeekDay.SA; } throw new RuntimeException(); } diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index 679631224..d9afed9a5 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.annotation.MainThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.google.ical.values.RRule import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.api.CaldavFilter @@ -30,6 +29,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.runBlocking +import net.fortuna.ical4j.model.Recur import org.tasks.Event import org.tasks.R import org.tasks.Strings @@ -39,6 +39,7 @@ import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.location.GeofenceApi import org.tasks.preferences.PermissionChecker import org.tasks.preferences.Preferences +import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.time.DateTime import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.startOfDay @@ -139,12 +140,14 @@ class TaskEditViewModel @Inject constructor( } } - var rrule: RRule? + var recur: Recur? get() = if (recurrence.isNullOrBlank()) { null } else { - val rrule = RRule(recurrence.withoutFrom()) - rrule.until = DateTime(repeatUntil!!).toDateValue() + val rrule = newRecur(recurrence!!) + repeatUntil?.takeIf { it > 0 }?.let { + rrule.until = DateTime(it).toDate() + } rrule } set(value) { @@ -153,16 +156,18 @@ class TaskEditViewModel @Inject constructor( repeatUntil = 0 return } - val copy: RRule = try { - RRule(value.toIcal()) + val copy = try { + newRecur(value.toString()) } catch (e: ParseException) { recurrence = "" repeatUntil = 0 return } repeatUntil = DateTime.from(copy.until).millis - copy.until = null - var result = copy.toIcal() + if (repeatUntil ?: 0 > 0) { + copy.until = null + } + var result = copy.toString() if (repeatAfterCompletion!! && !result.isNullOrBlank()) { result += ";FROM=COMPLETION" } diff --git a/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt b/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt index 813b4db85..4433b312f 100644 --- a/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt +++ b/app/src/test/java/org/tasks/caldav/AppleRemindersTests.kt @@ -55,7 +55,7 @@ class AppleRemindersTests { @Test fun repeatDaily() { - assertEquals("RRULE:FREQ=DAILY", vtodo("apple/repeat_daily.txt").recurrence) + assertEquals("FREQ=DAILY", vtodo("apple/repeat_daily.txt").recurrence) } @Test diff --git a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt index 3fbe35737..71935dbd9 100644 --- a/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt +++ b/app/src/test/java/org/tasks/caldav/ThunderbirdTests.kt @@ -59,7 +59,7 @@ class ThunderbirdTests { @Test fun repeatDaily() { assertEquals( - "RRULE:FREQ=DAILY", vtodo("thunderbird/repeat_daily.txt").recurrence) + "FREQ=DAILY", vtodo("thunderbird/repeat_daily.txt").recurrence) } @Test diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 63a46e267..34f313aa9 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -326,7 +326,6 @@ ++--- com.google.android.apps.dashclock:dashclock-api:2.0.0 ++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2 ++--- com.rubiconproject.oss:jchronic:0.2.6 -++--- org.scala-saddle:google-rfc-2445:20110304 ++--- com.wdullaer:materialdatetimepicker:4.2.3 +| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*) +| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*) diff --git a/deps_googleplay.txt b/deps_googleplay.txt index e5befc67e..fcc8f0e8e 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -437,7 +437,6 @@ ++--- com.google.android.apps.dashclock:dashclock-api:2.0.0 ++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2 ++--- com.rubiconproject.oss:jchronic:0.2.6 -++--- org.scala-saddle:google-rfc-2445:20110304 ++--- com.wdullaer:materialdatetimepicker:4.2.3 +| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*) +| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*)