Use ical4j instead of google-rfc-2445

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

@ -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")

@ -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

3
app/proguard.pro vendored

@ -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 <init>(**, android.view.View); }
-keep class butterknife.*

@ -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())
}
}

@ -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)
}

@ -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())

@ -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 &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": {
"name": "dagger",

@ -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)
}

@ -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(), "")
}
}

@ -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
}
}

@ -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<WeekdayNum>, date: DateTime): WeekdayNum {
private fun findNextWeekday(byDay: List<WeekDay>, 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)
}
}

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

@ -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
}
}

@ -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

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

@ -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<String> 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;
}
}

@ -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<String> 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<WeekdayNum> checked = new ArrayList<>();
if (rrule.getFrequency() == WEEKLY) {
List<WeekDay> 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();

@ -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())
}

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

@ -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();
}

@ -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"
}

@ -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

@ -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

@ -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 (*)

@ -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 (*)

Loading…
Cancel
Save