Use ical4j instead of google-rfc-2445

pull/1348/head
Alex Baker 5 years ago
parent c3080e6559
commit c4a97d2569

@ -190,9 +190,6 @@ dependencies {
implementation("com.rubiconproject.oss:jchronic:0.2.6") { implementation("com.rubiconproject.oss:jchronic:0.2.6") {
isTransitive = false isTransitive = false
} }
implementation("org.scala-saddle:google-rfc-2445:20110304") {
isTransitive = false
}
implementation("com.wdullaer:materialdatetimepicker:4.2.3") implementation("com.wdullaer:materialdatetimepicker:4.2.3")
implementation("me.leolin:ShortcutBadger:1.1.22@aar") implementation("me.leolin:ShortcutBadger:1.1.22@aar")
implementation("com.google.apis:google-api-services-tasks:v1-rev20200905-1.30.10") implementation("com.google.apis:google-api-services-tasks:v1-rev20200905-1.30.10")

@ -156,12 +156,6 @@
license: The Apache Software License, Version 2.0 license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html 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:+ - artifact: com.google.dagger:dagger:+
name: Dagger name: Dagger
copyrightHolder: The Dagger Authors copyrightHolder: The Dagger Authors

3
app/proguard.pro vendored

@ -9,9 +9,6 @@
public static *** i(...); public static *** i(...);
} }
# google-rfc-2445-20110304
-dontwarn com.google.ical.compat.jodatime.**
# https://github.com/JakeWharton/butterknife/blob/581666a28022796fdd62caaf3420e621215abfda/butterknife/proguard-rules.txt # https://github.com/JakeWharton/butterknife/blob/581666a28022796fdd62caaf3420e621215abfda/butterknife/proguard-rules.txt
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); } -keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.* -keep class butterknife.*

@ -283,19 +283,19 @@ class TitleParserTest : InjectingTestCase() {
val recur = newRecur() val recur = newRecur()
recur.setFrequency(DAILY.name) recur.setFrequency(DAILY.name)
recur.interval = 1 recur.interval = 1
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every day" title = "Jog every day"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i days." title = "Jog every $i days."
recur.interval = i recur.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
@ -309,19 +309,19 @@ class TitleParserTest : InjectingTestCase() {
val recur = newRecur() val recur = newRecur()
recur.setFrequency(WEEKLY.name) recur.setFrequency(WEEKLY.name)
recur.interval = 1 recur.interval = 1
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every week" title = "Jog every week"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i weeks" title = "Jog every $i weeks"
recur.interval = i recur.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
@ -335,19 +335,19 @@ class TitleParserTest : InjectingTestCase() {
val recur = newRecur() val recur = newRecur()
recur.setFrequency(MONTHLY.name) recur.setFrequency(MONTHLY.name)
recur.interval = 1 recur.interval = 1
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every month" title = "Jog every month"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i months" title = "Jog every $i months"
recur.interval = i recur.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
@ -360,17 +360,17 @@ class TitleParserTest : InjectingTestCase() {
val recur = newRecur() val recur = newRecur()
recur.setFrequency(DAILY.name) recur.setFrequency(DAILY.name)
recur.interval = 1 recur.interval = 1
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
title = "Jog every day starting from today" title = "Jog every day starting from today"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i days starting from today" title = "Jog every $i days starting from today"
recur.interval = i recur.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
} }
} }
@ -382,17 +382,17 @@ class TitleParserTest : InjectingTestCase() {
val recur = newRecur() val recur = newRecur()
recur.setFrequency(WEEKLY.name) recur.setFrequency(WEEKLY.name)
recur.interval = 1 recur.interval = 1
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
title = "Jog every week starting from today" title = "Jog every week starting from today"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i weeks starting from today" title = "Jog every $i weeks starting from today"
recur.interval = i recur.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, "RRULE:$recur") assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
} }
} }

@ -23,7 +23,7 @@ class RepeatTests : BaseTaskEditViewModelTest() {
save() save()
assertEquals( assertEquals(
"RRULE:FREQ=DAILY;INTERVAL=1;FROM=COMPLETION", "FREQ=DAILY;INTERVAL=1;FROM=COMPLETION",
taskDao.fetch(task.id)!!.recurrence) taskDao.fetch(task.id)!!.recurrence)
} }

@ -1,6 +1,5 @@
package org.tasks.makers package org.tasks.makers
import com.google.ical.values.RRule
import com.natpryce.makeiteasy.Instantiator import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.Property.newProperty import com.natpryce.makeiteasy.Property.newProperty
@ -84,8 +83,7 @@ object TaskMaker {
task.reminderPeriod = randomReminderPeriod task.reminderPeriod = randomReminderPeriod
} }
lookup.valueOf(RECUR, null as String?)?.let { lookup.valueOf(RECUR, null as String?)?.let {
val rrule = if (it.startsWith("RRULE:")) it else "RRULE:$it" task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false))
task.setRecurrence(RRule(rrule), lookup.valueOf(AFTER_COMPLETE, false))
} }
task.uuid = lookup.valueOf(UUID, NO_UUID) task.uuid = lookup.valueOf(UUID, NO_UUID)
val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime()) val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime())

@ -372,20 +372,6 @@
"url": "https://developer.android.com/topic/libraries/architecture/index.html", "url": "https://developer.android.com/topic/libraries/architecture/index.html",
"libraryName": "Android Lifecycle ViewModel" "libraryName": "Android Lifecycle ViewModel"
}, },
{
"artifactId": {
"name": "google-rfc-2445",
"group": "org.scala-saddle",
"version": "+"
},
"copyrightHolder": "Google Inc.",
"copyrightStatement": "Copyright &copy; 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": { "artifactId": {
"name": "dagger", "name": "dagger",

@ -32,7 +32,6 @@ import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import butterknife.OnClick import butterknife.OnClick
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.ical.values.RRule
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapter
@ -73,6 +72,7 @@ import org.tasks.locale.Locale
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Device import org.tasks.preferences.Device
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity import org.tasks.tags.TagPickerActivity
import org.tasks.tasklist.* import org.tasks.tasklist.*
@ -849,12 +849,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
task.setDueDateAdjustingHideUntil(oldDueDate) task.setDueDateAdjustingHideUntil(oldDueDate)
task.completionDate = 0L task.completionDate = 0L
try { try {
val rrule = RRule(task.getRecurrenceWithoutFrom()) val recur = newRecur(task.recurrence!!)
val count = rrule.count val count = recur.count
if (count > 0) { 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) { } catch (e: ParseException) {
Timber.e(e) Timber.e(e)
} }

@ -6,10 +6,10 @@ import android.os.Parcelable
import androidx.annotation.IntDef import androidx.annotation.IntDef
import androidx.core.os.ParcelCompat import androidx.core.os.ParcelCompat
import androidx.room.* import androidx.room.*
import com.google.ical.values.RRule
import com.todoroo.andlib.data.Table import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import net.fortuna.ical4j.model.Recur
import org.tasks.Strings import org.tasks.Strings
import org.tasks.data.Tag import org.tasks.data.Tag
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
@ -212,10 +212,6 @@ class Task : Parcelable {
fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion() fun repeatAfterCompletion(): Boolean = recurrence.isRepeatAfterCompletion()
fun sanitizedRecurrence(): String? = getRecurrenceWithoutFrom()?.sanitizeRRule()
fun getRecurrenceWithoutFrom(): String? = recurrence.withoutFrom()
fun setDueDateAdjustingHideUntil(newDueDate: Long) { fun setDueDateAdjustingHideUntil(newDueDate: Long) {
if (dueDate > 0) { if (dueDate > 0) {
if (hideUntil > 0) { if (hideUntil > 0) {
@ -228,17 +224,17 @@ class Task : Parcelable {
val isRecurring: Boolean val isRecurring: Boolean
get() = !Strings.isNullOrEmpty(recurrence) get() = !Strings.isNullOrEmpty(recurrence)
fun setRecurrence(rrule: RRule, afterCompletion: Boolean) { fun setRecurrence(rrule: String, afterCompletion: Boolean) {
recurrence = rrule.toIcal() + if (afterCompletion) ";FROM=COMPLETION" else "" recurrence = rrule + if (afterCompletion) ";FROM=COMPLETION" else ""
} }
fun setRecurrence(rrule: net.fortuna.ical4j.model.property.RRule?) { fun setRecurrence(rrule: Recur?) {
if (rrule == null) { if (rrule == null) {
repeatUntil = 0 repeatUntil = 0
recurrence = null recurrence = null
} else { } else {
repeatUntil = rrule.recur.until?.let { DateTime(it).millis } ?: 0 repeatUntil = rrule.until?.let { DateTime(it).millis } ?: 0
recurrence = "RRULE:${rrule.value.sanitizeRRule()}" + if (repeatAfterCompletion()) ";FROM=COMPLETION" else "" recurrence = rrule.toString() + if (repeatAfterCompletion()) ";FROM=COMPLETION" else ""
} }
} }
@ -603,9 +599,9 @@ class Task : Parcelable {
} }
@JvmStatic @JvmStatic
fun String?.sanitizeRRule(): String? = this fun String?.sanitizeRecur(): String? = this
?.replace("BYDAY=;", "") ?.replace("BYDAY=;", "")
?.replace(INVALID_COUNT, "") ?.replace(INVALID_COUNT, "") // ical4j adds COUNT=-1 if there is an UNTIL value
@JvmStatic fun isUuidEmpty(uuid: String?): Boolean { @JvmStatic fun isUuidEmpty(uuid: String?): Boolean {
return NO_UUID == uuid || Strings.isNullOrEmpty(uuid) return NO_UUID == uuid || Strings.isNullOrEmpty(uuid)
@ -614,8 +610,5 @@ class Task : Parcelable {
fun String?.isRepeatAfterCompletion() = this?.contains("FROM=COMPLETION") ?: false fun String?.isRepeatAfterCompletion() = this?.contains("FROM=COMPLETION") ?: false
fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "") fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "")
@JvmStatic
fun String?.withoutRRULE(): String? = this?.replace("^RRULE:".toRegex(), "")
} }
} }

@ -17,14 +17,14 @@ import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import butterknife.BindView import butterknife.BindView
import butterknife.OnItemSelected 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 dagger.hilt.android.AndroidEntryPoint
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import org.tasks.R import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.repeats.BasicRecurrenceDialog import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString import org.tasks.repeats.RepeatRuleToString
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.time.DateTime import org.tasks.time.DateTime
@ -63,9 +63,9 @@ class RepeatControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_RECURRENCE) { if (requestCode == REQUEST_RECURRENCE) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
viewModel.rrule = data viewModel.recur = data
?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE) ?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE)
?.let { RRule(it) } ?.let { newRecur(it) }
refreshDisplayView() refreshDisplayView()
} }
} else { } else {
@ -74,19 +74,22 @@ class RepeatControlSet : TaskEditControlFragment() {
} }
fun onDueDateChanged() { fun onDueDateChanged() {
viewModel.rrule?.let { viewModel.recur?.let { recur ->
if (it.freq == Frequency.MONTHLY && it.byDay.isNotEmpty()) { if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) {
val weekdayNum = it.byDay[0] val weekdayNum = recur.dayList[0]
val dateTime = DateTime(this.dueDate) val dateTime = DateTime(this.dueDate)
val num: Int val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth 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 if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth
} else { } else {
dayOfWeekInMonth dayOfWeekInMonth
} }
it.byDay = listOf((WeekdayNum(num, dateTime.weekday))) recur.dayList.let {
viewModel.rrule = it it.clear()
it.add(WeekDay(dateTime.weekDay, num))
}
viewModel.recur = recur
refreshDisplayView() refreshDisplayView()
} }
} }
@ -127,7 +130,7 @@ class RepeatControlSet : TaskEditControlFragment() {
override fun onRowClick() { override fun onRowClick() {
BasicRecurrenceDialog.newBasicRecurrenceDialog( BasicRecurrenceDialog.newBasicRecurrenceDialog(
this, REQUEST_RECURRENCE, viewModel.rrule, dueDate) this, REQUEST_RECURRENCE, viewModel.recur?.toString(), dueDate)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
} }
@ -140,12 +143,12 @@ class RepeatControlSet : TaskEditControlFragment() {
override fun controlId() = TAG override fun controlId() = TAG
private fun refreshDisplayView() { private fun refreshDisplayView() {
viewModel.rrule.let { viewModel.recur.let {
if (it == null) { if (it == null) {
displayView.text = null displayView.text = null
repeatTypeContainer.visibility = View.GONE repeatTypeContainer.visibility = View.GONE
} else { } else {
displayView.text = repeatRuleToString.toString(it.toIcal()) displayView.text = repeatRuleToString.toString(it)
repeatTypeContainer.visibility = View.VISIBLE repeatTypeContainer.visibility = View.VISIBLE
} }
} }

@ -5,20 +5,19 @@
*/ */
package com.todoroo.astrid.repeats 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.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate 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.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter 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.LocalBroadcastManager
import org.tasks.date.DateTimeUtils.newDate
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.newDateUtc import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime import org.tasks.time.DateTime
import timber.log.Timber import timber.log.Timber
import java.text.ParseException import java.text.ParseException
@ -33,13 +32,15 @@ class RepeatTaskHelper @Inject constructor(
private val taskCompleter: TaskCompleter, private val taskCompleter: TaskCompleter,
) { ) {
suspend fun handleRepeat(task: Task) { suspend fun handleRepeat(task: Task) {
val recurrence = task.sanitizedRecurrence() val recurrence = task.recurrence
if (recurrence.isNullOrBlank()) {
return
}
val repeatAfterCompletion = task.repeatAfterCompletion() val repeatAfterCompletion = task.repeatAfterCompletion()
if (!recurrence.isNullOrBlank()) {
val newDueDate: Long val newDueDate: Long
val rrule: RRule val rrule: Recur
try { try {
rrule = initRRule(task.recurrence) rrule = initRRule(recurrence)
newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion) newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion)
if (newDueDate == -1L) { if (newDueDate == -1L) {
return return
@ -59,7 +60,7 @@ class RepeatTaskHelper @Inject constructor(
} }
if (count > 1) { if (count > 1) {
rrule.count = count - 1 rrule.count = count - 1
task.setRecurrence(rrule, repeatAfterCompletion) task.setRecurrence(rrule.toString(), repeatAfterCompletion)
} }
task.reminderLast = 0L task.reminderLast = 0L
task.reminderSnooze = 0L task.reminderSnooze = 0L
@ -75,10 +76,9 @@ class RepeatTaskHelper @Inject constructor(
taskCompleter.completeChildren(task.id, 0L) taskCompleter.completeChildren(task.id, 0L)
localBroadcastManager.broadcastRepeat(task.id, previousDueDate, newDueDate) localBroadcastManager.broadcastRepeat(task.id, previousDueDate, newDueDate)
} }
}
companion object { 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 { private fun repeatFinished(newDueDate: Long, repeatUntil: Long): Boolean {
return (repeatUntil > 0 return (repeatUntil > 0
&& newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay())) && newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay()))
@ -86,17 +86,17 @@ class RepeatTaskHelper @Inject constructor(
/** Compute next due date */ /** Compute next due date */
@Throws(ParseException::class) @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) val rrule = initRRule(recurrence)
// initialize startDateAsDV // initialize startDateAsDV
val original = setUpStartDate(task, repeatAfterCompletion, rrule.freq) val original = setUpStartDate(task, repeatAfterCompletion, rrule.frequency)
val startDateAsDV = setUpStartDateAsDV(task, original) 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) 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()) 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) handleMonthlyRepeat(original, startDateAsDV, task.hasDueTime(), rrule)
} else { } else {
invokeRecurrence(rrule, original, startDateAsDV) invokeRecurrence(rrule, original, startDateAsDV)
@ -104,16 +104,16 @@ class RepeatTaskHelper @Inject constructor(
} }
private fun handleWeeklyRepeatAfterComplete( private fun handleWeeklyRepeatAfterComplete(
rrule: RRule, original: DateTime, hasDueTime: Boolean): Long { recur: Recur, original: DateTime, hasDueTime: Boolean): Long {
val byDay = rrule.byDay val byDay = recur.dayList
var newDate = original.millis var newDate = original.millis
newDate += DateUtilities.ONE_WEEK * (rrule.interval - 1) newDate += DateUtilities.ONE_WEEK * (recur.interval - 1)
var date = DateTime(newDate) var date = DateTime(newDate)
Collections.sort(byDay, weekdayCompare) Collections.sort(byDay, weekdayCompare)
val next = findNextWeekday(byDay, date) val next = findNextWeekday(byDay, date)
do { do {
date = date.plusDays(1) date = date.plusDays(1)
} while (date.dayOfWeek != next.wday.javaDayNum) } while (date.dayOfWeek != WeekDay.getCalendarDay(next))
val time = date.millis val time = date.millis
return if (hasDueTime) { return if (hasDueTime) {
createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time) createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time)
@ -123,9 +123,9 @@ class RepeatTaskHelper @Inject constructor(
} }
private fun handleMonthlyRepeat( 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) { return if (original.isLastDayOfMonth) {
val interval = rrule.interval val interval = recur.interval
val newDateTime = original.plusMonths(interval) val newDateTime = original.plusMonths(interval)
val time = newDateTime.withDayOfMonth(newDateTime.numberOfDaysInMonth).millis val time = newDateTime.withDayOfMonth(newDateTime.numberOfDaysInMonth).millis
if (hasDueTime) { if (hasDueTime) {
@ -134,90 +134,60 @@ class RepeatTaskHelper @Inject constructor(
createDueDate(Task.URGENCY_SPECIFIC_DAY, time) createDueDate(Task.URGENCY_SPECIFIC_DAY, time)
} }
} else { } else {
invokeRecurrence(rrule, original, startDateAsDV) invokeRecurrence(recur, original, startDateAsDV)
} }
} }
private fun findNextWeekday(byDay: List<WeekdayNum>, date: DateTime): WeekdayNum { private fun findNextWeekday(byDay: List<WeekDay>, date: DateTime): WeekDay {
val next = byDay[0] val next = byDay[0]
for (weekday in byDay) { for (weekday in byDay) {
if (weekday.wday.javaDayNum > date.dayOfWeek) { if (WeekDay.getCalendarDay(weekday) > date.dayOfWeek) {
return weekday return weekday
} }
} }
return next return next
} }
private fun invokeRecurrence(rrule: RRule, original: DateTime, startDateAsDV: DateValue): Long { private fun invokeRecurrence(recur: Recur, original: DateTime, startDateAsDV: Date): Long {
var newDueDate: Long = -1 val nextDate = recur.getNextDate(startDateAsDV, startDateAsDV)
val iterator = RecurrenceIteratorFactory.createRecurrenceIterator( return buildNewDueDate(original, nextDate)
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
} }
/** Compute long due date from DateValue */ /** 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 val newDueDate: Long
if (nextDate is DateTimeValueImpl) { if (nextDate is net.fortuna.ical4j.model.DateTime) {
var date = newDateUtc( var date = DateTime.from(nextDate)
nextDate.year(),
nextDate.month(),
nextDate.day(),
nextDate.hour(),
nextDate.minute(),
nextDate.second())
.toLocal()
// time may be inaccurate due to DST, force time to be same // time may be inaccurate due to DST, force time to be same
date = date.withHourOfDay(original.hourOfDay).withMinuteOfHour(original.minuteOfHour) date = date.withHourOfDay(original.hourOfDay).withMinuteOfHour(original.minuteOfHour)
newDueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.millis) newDueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.millis)
} else { } else {
newDueDate = createDueDate( newDueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY, Task.URGENCY_SPECIFIC_DAY,
newDate(nextDate.year(), nextDate.month(), nextDate.day()).millis) DateTime.from(nextDate).millis)
} }
return newDueDate return newDueDate
} }
/** Initialize RRule instance */ /** Initialize RRule instance */
@Throws(ParseException::class) @Throws(ParseException::class)
private fun initRRule(recurrence: String?): RRule { private fun initRRule(recurrence: String): Recur {
val rrule = RRule(recurrence val rrule = newRecur(recurrence)
?.let { if (it.startsWith("RRULE:")) it else "RRULE:$it" }
?.withoutFrom())
if (rrule.count < 0) {
rrule.count = 0
}
// handle the iCalendar "byDay" field differently depending on if // handle the iCalendar "byDay" field differently depending on if
// we are weekly or otherwise // we are weekly or otherwise
if (rrule.freq != Frequency.WEEKLY && rrule.freq != Frequency.MONTHLY) { if (rrule.frequency != Recur.Frequency.WEEKLY && rrule.frequency != Recur.Frequency.MONTHLY) {
rrule.byDay = emptyList() rrule.dayList.clear()
} }
return rrule return rrule
} }
/** Set up repeat start date */ /** Set up repeat start date */
private fun setUpStartDate( private fun setUpStartDate(
task: Task, repeatAfterCompletion: Boolean, frequency: Frequency): DateTime { task: Task, repeatAfterCompletion: Boolean, frequency: Recur.Frequency): DateTime {
return if (repeatAfterCompletion) { return if (repeatAfterCompletion) {
var startDate = if (task.isCompleted) newDateTime(task.completionDate) else newDateTime() 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) val dueDate = newDateTime(task.dueDate)
startDate = startDate startDate = startDate
.withHourOfDay(dueDate.hourOfDay) .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()) { return if (task.hasDueTime()) {
DateTimeValueImpl( startDate.toDateTime()
startDate.year,
startDate.monthOfYear,
startDate.dayOfMonth,
startDate.hourOfDay,
startDate.minuteOfHour,
startDate.secondOfMinute)
} else { } else {
DateValueImpl( startDate.toDate()
startDate.year, startDate.monthOfYear, startDate.dayOfMonth)
} }
} }
private fun handleSubdayRepeat(startDate: DateTime, rrule: RRule): Long { private fun handleSubdayRepeat(startDate: DateTime, recur: Recur): Long {
val millis: Long = when (rrule.freq) { val millis: Long = when (recur.frequency) {
Frequency.HOURLY -> DateUtilities.ONE_HOUR Recur.Frequency.HOURLY -> DateUtilities.ONE_HOUR
Frequency.MINUTELY -> DateUtilities.ONE_MINUTE Recur.Frequency.MINUTELY -> DateUtilities.ONE_MINUTE
else -> throw RuntimeException( 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) return createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDueDate)
} }
} }

@ -1,6 +1,5 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.google.ical.values.RRule
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
@ -105,7 +104,7 @@ class TaskCreator @Inject constructor(
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { ?.let {
task.setRecurrence( task.setRecurrence(
RRule(it), it,
preferences.getIntegerFromString(R.string.p_default_recurrence_from, 0) == 1) preferences.getIntegerFromString(R.string.p_default_recurrence_from, 0) == 1)
} }
preferences.getStringValue(R.string.p_default_location) preferences.getStringValue(R.string.p_default_location)

@ -5,14 +5,14 @@
*/ */
package com.todoroo.astrid.utility 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.AstridChronic
import com.mdimension.jchronic.Chronic import com.mdimension.jchronic.Chronic
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate import com.todoroo.astrid.data.Task.Companion.createDueDate
import net.fortuna.ical4j.model.Recur.Frequency
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.TagDataDao import org.tasks.data.TagDataDao
import org.tasks.repeats.RecurrenceUtils.newRecur
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.regex.Matcher import java.util.regex.Matcher
@ -373,10 +373,10 @@ object TitleParser {
val m = pattern.matcher(inputText) val m = pattern.matcher(inputText)
if (m.find()) { if (m.find()) {
val rtime = repeatTimes[repeatTime] val rtime = repeatTimes[repeatTime]
val rrule = RRule() val recur = newRecur()
rrule.freq = rtime recur.setFrequency(rtime!!.name)
rrule.interval = findInterval(inputText) recur.interval = findInterval(inputText)
task.recurrence = rrule.toIcal() task.recurrence = recur.toString()
return return
} }
} }
@ -385,11 +385,10 @@ object TitleParser {
val m = pattern.matcher(inputText) val m = pattern.matcher(inputText)
if (m.find()) { if (m.find()) {
val rtime = repeatTimesIntervalOne[repeatTimeIntervalOne] val rtime = repeatTimesIntervalOne[repeatTimeIntervalOne]
val rrule = RRule() val recur = newRecur()
rrule.freq = rtime recur.setFrequency(rtime!!.name)
rrule.interval = 1 recur.interval = 1
val thing = rrule.toIcal() task.recurrence = recur.toString()
task.recurrence = thing
return return
} }
} }

@ -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.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
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME 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.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.Date
@ -28,6 +26,8 @@ import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.Preferences 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.DateTime.UTC
import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.time.DateTimeUtils.startOfMinute import org.tasks.time.DateTimeUtils.startOfMinute
@ -205,18 +205,8 @@ class iCalendar @Inject constructor(
} }
@JvmStatic @JvmStatic
fun getLocal(property: DateProperty): Long { fun getLocal(property: DateProperty): Long =
val dateTime: org.tasks.time.DateTime? = if (property.date is DateTime) { org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0
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 fromVtodo(vtodo: String): Task? { fun fromVtodo(vtodo: String): Task? {
try { try {
@ -295,7 +285,7 @@ class iCalendar @Inject constructor(
in 6..9 -> com.todoroo.astrid.data.Task.Priority.LOW in 6..9 -> com.todoroo.astrid.data.Task.Priority.LOW
else -> com.todoroo.astrid.data.Task.Priority.NONE else -> com.todoroo.astrid.data.Task.Priority.NONE
} }
setRecurrence(remote.rRule) setRecurrence(remote.rRule?.recur)
remote.due.apply(this) remote.due.apply(this)
remote.dtStart.apply(this) remote.dtStart.apply(this)
} }
@ -333,12 +323,14 @@ class iCalendar @Inject constructor(
} }
rRule = if (task.isRecurring) { rRule = if (task.isRecurring) {
try { try {
val rrule = RRule(task.getRecurrenceWithoutFrom().withoutRRULE()) val recur = newRecur(task.recurrence!!)
val repeatUntil = task.repeatUntil val repeatUntil = task.repeatUntil
rrule recur.until = if (repeatUntil > 0) {
.recur.until = if (repeatUntil > 0) DateTime(newDateTime(repeatUntil).toUTC().millis) else null DateTime(newDateTime(repeatUntil).toUTC().millis)
val sanitized: String = rrule.value.sanitizeRRule()!! // ical4j adds COUNT=-1 if there is an UNTIL value } else {
RRule(sanitized) null
}
newRRule(recur.toString())
} catch (e: ParseException) { } catch (e: ParseException) {
Timber.e(e) Timber.e(e)
null null

@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.google.ical.values.RRule
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
@ -78,10 +77,9 @@ class TaskDefaults : InjectingPreferenceFragment() {
findPreference(R.string.p_default_recurrence) findPreference(R.string.p_default_recurrence)
.setOnPreferenceClickListener { .setOnPreferenceClickListener {
val rrule: RRule? = preferences val rrule = preferences
.getStringValue(R.string.p_default_recurrence) .getStringValue(R.string.p_default_recurrence)
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { RRule(it) }
BasicRecurrenceDialog BasicRecurrenceDialog
.newBasicRecurrenceDialog(this, REQUEST_RECURRENCE, rrule, -1) .newBasicRecurrenceDialog(this, REQUEST_RECURRENCE, rrule, -1)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)

@ -2,14 +2,15 @@ package org.tasks.repeats;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayList;
import static com.google.ical.values.Frequency.DAILY; import static net.fortuna.ical4j.model.Recur.Frequency.DAILY;
import static com.google.ical.values.Frequency.HOURLY; import static net.fortuna.ical4j.model.Recur.Frequency.HOURLY;
import static com.google.ical.values.Frequency.MINUTELY; import static net.fortuna.ical4j.model.Recur.Frequency.MINUTELY;
import static com.google.ical.values.Frequency.MONTHLY; import static net.fortuna.ical4j.model.Recur.Frequency.MONTHLY;
import static com.google.ical.values.Frequency.WEEKLY; import static net.fortuna.ical4j.model.Recur.Frequency.WEEKLY;
import static com.google.ical.values.Frequency.YEARLY; import static net.fortuna.ical4j.model.Recur.Frequency.YEARLY;
import static org.tasks.Strings.isNullOrEmpty; import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.repeats.CustomRecurrenceDialog.newCustomRecurrenceDialog; import static org.tasks.repeats.CustomRecurrenceDialog.newCustomRecurrenceDialog;
import static org.tasks.repeats.RecurrenceUtils.newRecur;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import android.app.Activity; import android.app.Activity;
@ -20,11 +21,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.Recur.Frequency;
import org.tasks.R; import org.tasks.R;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.ui.SingleCheckedArrayAdapter; import org.tasks.ui.SingleCheckedArrayAdapter;
@ -42,12 +43,12 @@ public class BasicRecurrenceDialog extends DialogFragment {
@Inject RepeatRuleToString repeatRuleToString; @Inject RepeatRuleToString repeatRuleToString;
public static BasicRecurrenceDialog newBasicRecurrenceDialog( public static BasicRecurrenceDialog newBasicRecurrenceDialog(
Fragment target, int rc, RRule rrule, long dueDate) { Fragment target, int rc, String rrule, long dueDate) {
BasicRecurrenceDialog dialog = new BasicRecurrenceDialog(); BasicRecurrenceDialog dialog = new BasicRecurrenceDialog();
dialog.setTargetFragment(target, rc); dialog.setTargetFragment(target, rc);
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
if (rrule != null) { if (rrule != null) {
arguments.putString(EXTRA_RRULE, rrule.toIcal()); arguments.putString(EXTRA_RRULE, rrule);
} }
arguments.putLong(EXTRA_DATE, dueDate); arguments.putLong(EXTRA_DATE, dueDate);
dialog.setArguments(arguments); dialog.setArguments(arguments);
@ -60,15 +61,14 @@ public class BasicRecurrenceDialog extends DialogFragment {
Bundle arguments = getArguments(); Bundle arguments = getArguments();
long dueDate = arguments.getLong(EXTRA_DATE, currentTimeMillis()); long dueDate = arguments.getLong(EXTRA_DATE, currentTimeMillis());
String rule = arguments.getString(EXTRA_RRULE); String rule = arguments.getString(EXTRA_RRULE);
RRule parsed = null; Recur rrule = null;
try { try {
if (!isNullOrEmpty(rule)) { if (!isNullOrEmpty(rule)) {
parsed = new RRule(rule); rrule = newRecur(rule);
} }
} catch (Exception e) { } catch (Exception e) {
Timber.e(e); Timber.e(e);
} }
RRule rrule = parsed;
boolean customPicked = isCustomValue(rrule); boolean customPicked = isCustomValue(rrule);
List<String> repeatOptions = List<String> repeatOptions =
@ -79,7 +79,7 @@ public class BasicRecurrenceDialog extends DialogFragment {
if (customPicked) { if (customPicked) {
adapter.insert(repeatRuleToString.toString(rule), 0); adapter.insert(repeatRuleToString.toString(rule), 0);
} else if (rrule != null) { } else if (rrule != null) {
switch (rrule.getFreq()) { switch (rrule.getFrequency()) {
case DAILY: case DAILY:
selected = 1; selected = 1;
break; break;
@ -110,37 +110,37 @@ public class BasicRecurrenceDialog extends DialogFragment {
} }
i--; i--;
} }
RRule result; Recur result;
if (i == 0) { if (i == 0) {
result = null; result = null;
} else if (i == 5) { } else if (i == 5) {
newCustomRecurrenceDialog( newCustomRecurrenceDialog(
getTargetFragment(), getTargetRequestCode(), rrule, dueDate) getTargetFragment(), getTargetRequestCode(), rule, dueDate)
.show(getParentFragmentManager(), FRAG_TAG_CUSTOM_RECURRENCE); .show(getParentFragmentManager(), FRAG_TAG_CUSTOM_RECURRENCE);
dialogInterface.dismiss(); dialogInterface.dismiss();
return; return;
} else { } else {
result = new RRule(); result = newRecur();
result.setInterval(1); result.setInterval(1);
switch (i) { switch (i) {
case 1: case 1:
result.setFreq(DAILY); result.setFrequency(DAILY.name());
break; break;
case 2: case 2:
result.setFreq(WEEKLY); result.setFrequency(WEEKLY.name());
break; break;
case 3: case 3:
result.setFreq(MONTHLY); result.setFrequency(MONTHLY.name());
break; break;
case 4: case 4:
result.setFreq(YEARLY); result.setFrequency(YEARLY.name());
break; break;
} }
} }
Intent intent = new Intent(); 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); getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent);
dialogInterface.dismiss(); dialogInterface.dismiss();
}) })
@ -148,16 +148,16 @@ public class BasicRecurrenceDialog extends DialogFragment {
.show(); .show();
} }
private boolean isCustomValue(RRule rrule) { private boolean isCustomValue(Recur rrule) {
if (rrule == null) { if (rrule == null) {
return false; return false;
} }
Frequency frequency = rrule.getFreq(); Frequency frequency = rrule.getFrequency();
return (frequency == WEEKLY || frequency == MONTHLY) && !rrule.getByDay().isEmpty() return (frequency == WEEKLY || frequency == MONTHLY) && !rrule.getDayList().isEmpty()
|| frequency == HOURLY || frequency == HOURLY
|| frequency == MINUTELY || frequency == MINUTELY
|| rrule.getUntil() != null || rrule.getUntil() != null
|| rrule.getInterval() != 1 || rrule.getInterval() > 1
|| rrule.getCount() != 0; || rrule.getCount() > 0;
} }
} }

@ -2,16 +2,17 @@ package org.tasks.repeats;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static com.google.common.collect.Lists.newArrayList; 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 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.Strings.isNullOrEmpty;
import static org.tasks.dialogs.MyDatePickerDialog.newDatePicker; import static org.tasks.dialogs.MyDatePickerDialog.newDatePicker;
import static org.tasks.repeats.BasicRecurrenceDialog.EXTRA_RRULE; import static org.tasks.repeats.BasicRecurrenceDialog.EXTRA_RRULE;
import static org.tasks.repeats.RecurrenceUtils.newRecur;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import android.app.Activity; import android.app.Activity;
@ -46,19 +47,17 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnItemSelected; import butterknife.OnItemSelected;
import butterknife.OnTextChanged; 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 com.todoroo.andlib.utility.DateUtilities;
import dagger.hilt.android.AndroidEntryPoint; import dagger.hilt.android.AndroidEntryPoint;
import java.text.DateFormatSymbols; import java.text.DateFormatSymbols;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject; 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.R;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.MyDatePickerDialog; import org.tasks.dialogs.MyDatePickerDialog;
@ -140,16 +139,16 @@ public class CustomRecurrenceDialog extends DialogFragment {
private ArrayAdapter<String> repeatUntilAdapter; private ArrayAdapter<String> repeatUntilAdapter;
private ToggleButton[] weekButtons; private ToggleButton[] weekButtons;
private RRule rrule; private Recur rrule;
private long dueDate; private long dueDate;
public static CustomRecurrenceDialog newCustomRecurrenceDialog( public static CustomRecurrenceDialog newCustomRecurrenceDialog(
Fragment target, int rc, RRule rrule, long dueDate) { Fragment target, int rc, String rrule, long dueDate) {
CustomRecurrenceDialog dialog = new CustomRecurrenceDialog(); CustomRecurrenceDialog dialog = new CustomRecurrenceDialog();
dialog.setTargetFragment(target, rc); dialog.setTargetFragment(target, rc);
Bundle arguments = new Bundle(); Bundle arguments = new Bundle();
if (rrule != null) { if (rrule != null) {
arguments.putString(EXTRA_RRULE, rrule.toIcal()); arguments.putString(EXTRA_RRULE, rrule);
} }
arguments.putLong(EXTRA_DATE, dueDate); arguments.putLong(EXTRA_DATE, dueDate);
dialog.setArguments(arguments); dialog.setArguments(arguments);
@ -170,15 +169,15 @@ public class CustomRecurrenceDialog extends DialogFragment {
: savedInstanceState.getString(EXTRA_RRULE); : savedInstanceState.getString(EXTRA_RRULE);
try { try {
if (!isNullOrEmpty(rule)) { if (!isNullOrEmpty(rule)) {
rrule = new RRule(rule); rrule = newRecur(rule);
} }
} catch (Exception e) { } catch (Exception e) {
Timber.e(e); Timber.e(e);
} }
if (rrule == null) { if (rrule == null) {
rrule = new RRule(); rrule = newRecur();
rrule.setInterval(1); rrule.setInterval(1);
rrule.setFreq(WEEKLY); rrule.setFrequency(WEEKLY.name());
} }
DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale()); DateFormatSymbols dfs = new DateFormatSymbols(locale.getLocale());
@ -197,7 +196,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
repeatMonthlyDayOfLastWeek.setVisibility(View.VISIBLE); repeatMonthlyDayOfLastWeek.setVisibility(View.VISIBLE);
String last = getString(R.string.repeat_monthly_last_week); String last = getString(R.string.repeat_monthly_last_week);
String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, last, today); 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); repeatMonthlyDayOfLastWeek.setText(text);
} else { } else {
repeatMonthlyDayOfLastWeek.setVisibility(View.GONE); repeatMonthlyDayOfLastWeek.setVisibility(View.GONE);
@ -216,18 +215,18 @@ public class CustomRecurrenceDialog extends DialogFragment {
String nth = getString(resources[dayOfWeekInMonth - 1]); String nth = getString(resources[dayOfWeekInMonth - 1]);
String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, nth, today); String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, nth, today);
repeatMonthlyDayOfNthWeek.setTag( repeatMonthlyDayOfNthWeek.setTag(
new WeekdayNum(dayOfWeekInMonth, calendarDayToWeekday(dueDayOfWeek))); new WeekDay(calendarDayToWeekday(dueDayOfWeek), dayOfWeekInMonth));
repeatMonthlyDayOfNthWeek.setText(text); repeatMonthlyDayOfNthWeek.setText(text);
} else { } else {
repeatMonthlyDayOfNthWeek.setVisibility(View.GONE); repeatMonthlyDayOfNthWeek.setVisibility(View.GONE);
} }
if (rrule.getFreq() == MONTHLY) { if (rrule.getFrequency() == MONTHLY) {
if (rrule.getByDay().size() == 1) { if (rrule.getDayList().size() == 1) {
WeekdayNum weekdayNum = rrule.getByDay().get(0); WeekDay weekday = rrule.getDayList().get(0);
if (weekdayNum.num == -1) { if (weekday.getOffset() == -1) {
repeatMonthlyDayOfLastWeek.setChecked(true); repeatMonthlyDayOfLastWeek.setChecked(true);
} else if (weekdayNum.num == dayOfWeekInMonth) { } else if (weekday.getOffset() == dayOfWeekInMonth) {
repeatMonthlyDayOfNthWeek.setChecked(true); repeatMonthlyDayOfNthWeek.setChecked(true);
} }
} }
@ -241,7 +240,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
ArrayAdapter.createFromResource(context, R.array.repeat_frequency, R.layout.frequency_item); ArrayAdapter.createFromResource(context, R.array.repeat_frequency, R.layout.frequency_item);
frequencyAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); frequencyAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
frequencySpinner.setAdapter(frequencyAdapter); frequencySpinner.setAdapter(frequencyAdapter);
frequencySpinner.setSelection(FREQUENCIES.indexOf(rrule.getFreq())); frequencySpinner.setSelection(FREQUENCIES.indexOf(rrule.getFrequency()));
intervalEditText.setText(locale.formatNumber(rrule.getInterval())); intervalEditText.setText(locale.formatNumber(rrule.getInterval()));
@ -280,7 +279,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
Calendar dayOfWeekCalendar = Calendar.getInstance(locale.getLocale()); Calendar dayOfWeekCalendar = Calendar.getInstance(locale.getLocale());
dayOfWeekCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeekCalendar.getFirstDayOfWeek()); 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 = ColorStateList colorStateList =
new ColorStateList( new ColorStateList(
@ -323,12 +322,12 @@ public class CustomRecurrenceDialog extends DialogFragment {
weekButton.setTextColor(colorStateList); weekButton.setTextColor(colorStateList);
weekButton.setTextOn(text); weekButton.setTextOn(text);
weekButton.setTextOff(text); weekButton.setTextOff(text);
weekButton.setTag(new WeekdayNum(0, calendarDayToWeekday(dayOfWeek))); weekButton.setTag(new WeekDay(calendarDayToWeekday(dayOfWeek), 0));
if (savedInstanceState == null) { if (savedInstanceState == null) {
weekButton.setChecked( weekButton.setChecked(
rrule.getFreq() != WEEKLY || rrule.getByDay().isEmpty() rrule.getFrequency() != WEEKLY || rrule.getDayList().isEmpty()
? todayWeekday.equals(weekButton.getTag()) ? todayWeekday.equals(weekButton.getTag())
: rrule.getByDay().contains(weekButton.getTag())); : rrule.getDayList().contains(weekButton.getTag()));
} }
dayOfWeekCalendar.add(Calendar.DATE, 1); dayOfWeekCalendar.add(Calendar.DATE, 1);
} }
@ -344,51 +343,54 @@ public class CustomRecurrenceDialog extends DialogFragment {
} }
private void onRuleSelected(DialogInterface dialogInterface, int which) { private void onRuleSelected(DialogInterface dialogInterface, int which) {
if (rrule.getFreq() == WEEKLY) { if (rrule.getFrequency() == WEEKLY) {
List<WeekdayNum> checked = new ArrayList<>(); List<WeekDay> checked = new ArrayList<>();
for (ToggleButton weekButton : weekButtons) { for (ToggleButton weekButton : weekButtons) {
if (weekButton.isChecked()) { if (weekButton.isChecked()) {
checked.add((WeekdayNum) weekButton.getTag()); checked.add((WeekDay) weekButton.getTag());
} }
} }
rrule.setByDay(checked); rrule.getDayList().clear();
} else if (rrule.getFreq() == MONTHLY) { rrule.getDayList().addAll(checked);
} else if (rrule.getFrequency() == MONTHLY) {
switch (monthGroup.getCheckedRadioButtonId()) { switch (monthGroup.getCheckedRadioButtonId()) {
case R.id.repeat_monthly_same_day: case R.id.repeat_monthly_same_day:
rrule.setByDay(Collections.emptyList()); rrule.getDayList().clear();
break; break;
case R.id.repeat_monthly_day_of_nth_week: 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; break;
case R.id.repeat_monthly_day_of_last_week: 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; break;
} }
} else { } else {
rrule.setByDay(Collections.emptyList()); rrule.getDayList().clear();
} }
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(EXTRA_RRULE, rrule.toIcal()); intent.putExtra(EXTRA_RRULE, rrule.toString());
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent); getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent);
dismiss(); dismiss();
} }
private Weekday calendarDayToWeekday(int calendarDay) { private WeekDay calendarDayToWeekday(int calendarDay) {
switch (calendarDay) { switch (calendarDay) {
case Calendar.SUNDAY: case Calendar.SUNDAY:
return Weekday.SU; return WeekDay.SU;
case Calendar.MONDAY: case Calendar.MONDAY:
return Weekday.MO; return WeekDay.MO;
case Calendar.TUESDAY: case Calendar.TUESDAY:
return Weekday.TU; return WeekDay.TU;
case Calendar.WEDNESDAY: case Calendar.WEDNESDAY:
return Weekday.WE; return WeekDay.WE;
case Calendar.THURSDAY: case Calendar.THURSDAY:
return Weekday.TH; return WeekDay.TH;
case Calendar.FRIDAY: case Calendar.FRIDAY:
return Weekday.FR; return WeekDay.FR;
case Calendar.SATURDAY: case Calendar.SATURDAY:
return Weekday.SA; return WeekDay.SA;
} }
throw new RuntimeException("Invalid calendar day: " + calendarDay); throw new RuntimeException("Invalid calendar day: " + calendarDay);
} }
@ -397,7 +399,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(EXTRA_RRULE, rrule.toIcal()); outState.putString(EXTRA_RRULE, rrule.toString());
} }
private void setInterval(int interval, boolean updateEditText) { private void setInterval(int interval, boolean updateEditText) {
@ -428,7 +430,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
} }
private int getFrequencyPlural() { private int getFrequencyPlural() {
switch (rrule.getFreq()) { switch (rrule.getFrequency()) {
case MINUTELY: case MINUTELY:
return R.plurals.repeat_minutes; return R.plurals.repeat_minutes;
case HOURLY: case HOURLY:
@ -442,7 +444,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
case YEARLY: case YEARLY:
return R.plurals.repeat_years; return R.plurals.repeat_years;
default: 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) @OnItemSelected(R.id.frequency)
public void onFrequencyChanged(int position) { public void onFrequencyChanged(int position) {
Frequency frequency = FREQUENCIES.get(position); Frequency frequency = FREQUENCIES.get(position);
rrule.setFreq(frequency); rrule.setFrequency(frequency.name());
int weekVisibility = frequency == WEEKLY ? View.VISIBLE : View.GONE; int weekVisibility = frequency == WEEKLY ? View.VISIBLE : View.GONE;
weekGroup1.setVisibility(weekVisibility); weekGroup1.setVisibility(weekVisibility);
if (weekGroup2 != null) { if (weekGroup2 != null) {
@ -545,7 +547,7 @@ public class CustomRecurrenceDialog extends DialogFragment {
if (requestCode == REQUEST_PICK_DATE) { if (requestCode == REQUEST_PICK_DATE) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
rrule.setUntil( rrule.setUntil(
new DateTime(data.getLongExtra(MyDatePickerDialog.EXTRA_TIMESTAMP, 0L)).toDateValue()); new DateTime(data.getLongExtra(MyDatePickerDialog.EXTRA_TIMESTAMP, 0L)).toDate());
rrule.setCount(0); rrule.setCount(0);
} }
updateRepeatUntilOptions(); updateRepeatUntilOptions();

@ -1,5 +1,6 @@
package org.tasks.repeats package org.tasks.repeats
import com.todoroo.astrid.data.Task.Companion.sanitizeRecur
import com.todoroo.astrid.data.Task.Companion.withoutFrom import com.todoroo.astrid.data.Task.Companion.withoutFrom
import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.property.RRule import net.fortuna.ical4j.model.property.RRule
@ -14,6 +15,6 @@ object RecurrenceUtils {
fun newRecur(rrule: String): Recur = newRRule(rrule).recur fun newRecur(rrule: String): Recur = newRRule(rrule).recur
fun newRRule(rrule: String): RRule = fun newRRule(rrule: String): RRule =
RRule(rrule.replace(LEGACY_RRULE_PREFIX, "").withoutFrom()) RRule(rrule.replace(LEGACY_RRULE_PREFIX, "").withoutFrom().sanitizeRecur())
} }

@ -2,17 +2,16 @@ package org.tasks.repeats
import android.content.Context import android.content.Context
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task.Companion.withoutRRULE
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.Recur 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.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.WeekDay.Day.* import net.fortuna.ical4j.model.WeekDay.Day.*
import net.fortuna.ical4j.model.property.RRule
import org.tasks.R import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.locale.Locale import org.tasks.locale.Locale
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.text.ParseException import java.text.ParseException
@ -27,14 +26,13 @@ class RepeatRuleToString @Inject constructor(
private val weekdays = listOf(*Day.values()) private val weekdays = listOf(*Day.values())
fun toString(rrule: String?): String? = try { fun toString(rrule: String?): String? = try {
toString(RRule(rrule.withoutRRULE())) rrule?.let { toString(newRecur(it)) }
} catch (e: ParseException) { } catch (e: ParseException) {
firebase.reportException(e) firebase.reportException(e)
null null
} }
fun toString(r: RRule): String { fun toString(rrule: Recur): String {
val rrule = r.recur
val interval = rrule.interval val interval = rrule.interval
val frequency = rrule.frequency val frequency = rrule.frequency
val repeatUntil = if (rrule.until == null) null else DateTime.from(rrule.until) val repeatUntil = if (rrule.until == null) null else DateTime.from(rrule.until)

@ -10,10 +10,6 @@ import static java.util.Calendar.TUESDAY;
import static java.util.Calendar.WEDNESDAY; import static java.util.Calendar.WEDNESDAY;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; 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.text.SimpleDateFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -23,6 +19,7 @@ import java.util.GregorianCalendar;
import java.util.Objects; import java.util.Objects;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.fortuna.ical4j.model.WeekDay;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
public class DateTime { public class DateTime {
@ -86,19 +83,24 @@ public class DateTime {
} }
public static DateTime from(Date date) { public static DateTime from(Date date) {
if (date == null) {
return new DateTime(0);
}
DateTime dateTime = new DateTime(date.getTime()); DateTime dateTime = new DateTime(date.getTime());
return dateTime.minusMillis(dateTime.getOffset()); return dateTime.minusMillis(dateTime.getOffset());
} }
public static DateTime from(DateValue dateValue) { public static DateTime from(net.fortuna.ical4j.model.Date date) {
if (dateValue == null) { if (date instanceof net.fortuna.ical4j.model.DateTime) {
return new DateTime(0); net.fortuna.ical4j.model.DateTime dt = (net.fortuna.ical4j.model.DateTime) date;
} TimeZone tz = dt.getTimeZone();
if (dateValue instanceof DateTimeValue) { return new DateTime(
DateTimeValue dt = (DateTimeValue) dateValue; dt.getTime(),
return new DateTime(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), dt.second()); 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) { public DateTime(Date date) {
@ -358,8 +360,8 @@ public class DateTime {
return timestamp == 0 ? null : new net.fortuna.ical4j.model.DateTime(timestamp); return timestamp == 0 ? null : new net.fortuna.ical4j.model.DateTime(timestamp);
} }
public DateValue toDateValue() { public net.fortuna.ical4j.model.Date toDate() {
return timestamp == 0 ? null : new DateValueImpl(getYear(), getMonthOfYear(), getDayOfMonth()); return timestamp == 0 ? null : new net.fortuna.ical4j.model.Date(timestamp);
} }
public LocalDate toLocalDate() { public LocalDate toLocalDate() {
@ -382,22 +384,22 @@ public class DateTime {
return getCalendar().getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH); return getCalendar().getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
} }
public Weekday getWeekday() { public WeekDay getWeekDay() {
switch (getCalendar().get(Calendar.DAY_OF_WEEK)) { switch (getCalendar().get(Calendar.DAY_OF_WEEK)) {
case SUNDAY: case SUNDAY:
return Weekday.SU; return WeekDay.SU;
case MONDAY: case MONDAY:
return Weekday.MO; return WeekDay.MO;
case TUESDAY: case TUESDAY:
return Weekday.TU; return WeekDay.TU;
case WEDNESDAY: case WEDNESDAY:
return Weekday.WE; return WeekDay.WE;
case THURSDAY: case THURSDAY:
return Weekday.TH; return WeekDay.TH;
case FRIDAY: case FRIDAY:
return Weekday.FR; return WeekDay.FR;
case SATURDAY: case SATURDAY:
return Weekday.SA; return WeekDay.SA;
} }
throw new RuntimeException(); throw new RuntimeException();
} }

@ -4,7 +4,6 @@ import android.content.Context
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.google.ical.values.RRule
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
@ -30,6 +29,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.fortuna.ical4j.model.Recur
import org.tasks.Event import org.tasks.Event
import org.tasks.R import org.tasks.R
import org.tasks.Strings import org.tasks.Strings
@ -39,6 +39,7 @@ import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfDay
@ -139,12 +140,14 @@ class TaskEditViewModel @Inject constructor(
} }
} }
var rrule: RRule? var recur: Recur?
get() = if (recurrence.isNullOrBlank()) { get() = if (recurrence.isNullOrBlank()) {
null null
} else { } else {
val rrule = RRule(recurrence.withoutFrom()) val rrule = newRecur(recurrence!!)
rrule.until = DateTime(repeatUntil!!).toDateValue() repeatUntil?.takeIf { it > 0 }?.let {
rrule.until = DateTime(it).toDate()
}
rrule rrule
} }
set(value) { set(value) {
@ -153,16 +156,18 @@ class TaskEditViewModel @Inject constructor(
repeatUntil = 0 repeatUntil = 0
return return
} }
val copy: RRule = try { val copy = try {
RRule(value.toIcal()) newRecur(value.toString())
} catch (e: ParseException) { } catch (e: ParseException) {
recurrence = "" recurrence = ""
repeatUntil = 0 repeatUntil = 0
return return
} }
repeatUntil = DateTime.from(copy.until).millis repeatUntil = DateTime.from(copy.until).millis
if (repeatUntil ?: 0 > 0) {
copy.until = null copy.until = null
var result = copy.toIcal() }
var result = copy.toString()
if (repeatAfterCompletion!! && !result.isNullOrBlank()) { if (repeatAfterCompletion!! && !result.isNullOrBlank()) {
result += ";FROM=COMPLETION" result += ";FROM=COMPLETION"
} }

@ -55,7 +55,7 @@ class AppleRemindersTests {
@Test @Test
fun repeatDaily() { fun repeatDaily() {
assertEquals("RRULE:FREQ=DAILY", vtodo("apple/repeat_daily.txt").recurrence) assertEquals("FREQ=DAILY", vtodo("apple/repeat_daily.txt").recurrence)
} }
@Test @Test

@ -59,7 +59,7 @@ class ThunderbirdTests {
@Test @Test
fun repeatDaily() { fun repeatDaily() {
assertEquals( assertEquals(
"RRULE:FREQ=DAILY", vtodo("thunderbird/repeat_daily.txt").recurrence) "FREQ=DAILY", vtodo("thunderbird/repeat_daily.txt").recurrence)
} }
@Test @Test

@ -326,7 +326,6 @@
++--- com.google.android.apps.dashclock:dashclock-api:2.0.0 ++--- com.google.android.apps.dashclock:dashclock-api:2.0.0
++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2 ++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2
++--- com.rubiconproject.oss:jchronic:0.2.6 ++--- com.rubiconproject.oss:jchronic:0.2.6
++--- org.scala-saddle:google-rfc-2445:20110304
++--- com.wdullaer:materialdatetimepicker:4.2.3 ++--- com.wdullaer:materialdatetimepicker:4.2.3
+| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*) +| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*)
+| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*) +| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*)

@ -437,7 +437,6 @@
++--- com.google.android.apps.dashclock:dashclock-api:2.0.0 ++--- com.google.android.apps.dashclock:dashclock-api:2.0.0
++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2 ++--- com.twofortyfouram:android-plugin-api-for-locale:1.0.2
++--- com.rubiconproject.oss:jchronic:0.2.6 ++--- com.rubiconproject.oss:jchronic:0.2.6
++--- org.scala-saddle:google-rfc-2445:20110304
++--- com.wdullaer:materialdatetimepicker:4.2.3 ++--- com.wdullaer:materialdatetimepicker:4.2.3
+| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*) +| +--- androidx.appcompat:appcompat:1.0.2 -> 1.2.0 (*)
+| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*) +| \--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0 (*)

Loading…
Cancel
Save