Properly handle all-day event timezone switch

This commit is contained in:
Naveen 2022-10-31 05:06:54 +05:30
parent d485be7cf4
commit 61ed3a2b90
8 changed files with 94 additions and 53 deletions

View File

@ -161,7 +161,7 @@ class EventActivity : SimpleActivity() {
event_end_time.setOnClickListener { setupEndTime() } event_end_time.setOnClickListener { setupEndTime() }
event_time_zone.setOnClickListener { setupTimeZone() } event_time_zone.setOnClickListener { setupTimeZone() }
event_all_day.setOnCheckedChangeListener { compoundButton, isChecked -> toggleAllDay(isChecked) } event_all_day.setOnCheckedChangeListener { _, isChecked -> toggleAllDay(isChecked) }
event_repetition.setOnClickListener { showRepeatIntervalDialog() } event_repetition.setOnClickListener { showRepeatIntervalDialog() }
event_repetition_rule_holder.setOnClickListener { showRepetitionRuleDialog() } event_repetition_rule_holder.setOnClickListener { showRepetitionRuleDialog() }
event_repetition_limit_holder.setOnClickListener { showRepetitionTypePicker() } event_repetition_limit_holder.setOnClickListener { showRepetitionTypePicker() }
@ -213,7 +213,7 @@ class EventActivity : SimpleActivity() {
event_type_holder.setOnClickListener { showEventTypeDialog() } event_type_holder.setOnClickListener { showEventTypeDialog() }
event_all_day.apply { event_all_day.apply {
isChecked = mEvent.flags and FLAG_ALL_DAY != 0 isChecked = mEvent.getIsAllDay()
jumpDrawablesToCurrentState() jumpDrawablesToCurrentState()
} }
@ -227,13 +227,6 @@ class EventActivity : SimpleActivity() {
showOrHideTimeZone() showOrHideTimeZone()
} }
private fun showOrHideTimeZone() {
val allowChangingTimeZones = config.allowChangingTimeZones && !event_all_day.isChecked
event_time_zone_divider.beVisibleIf(allowChangingTimeZones)
event_time_zone_image.beVisibleIf(allowChangingTimeZones)
event_time_zone.beVisibleIf(allowChangingTimeZones)
}
private fun refreshMenuItems() { private fun refreshMenuItems() {
if (::mEvent.isInitialized) { if (::mEvent.isInitialized) {
event_toolbar.menu.apply { event_toolbar.menu.apply {
@ -258,17 +251,25 @@ class EventActivity : SimpleActivity() {
} }
private fun getStartEndTimes(): Pair<Long, Long> { private fun getStartEndTimes(): Pair<Long, Long> {
val offset = if (!config.allowChangingTimeZones || mEvent.getTimeZoneString().equals(mOriginalTimeZone, true)) { if (mIsAllDayEvent) {
0 val newStartTS = mEventStartDateTime.withTimeAtStartOfDay().seconds()
val newEndTS = mEventEndDateTime.withTimeAtStartOfDay().withHourOfDay(12).seconds()
return Pair(newStartTS, newEndTS)
} else { } else {
val original = if (mOriginalTimeZone.isEmpty()) DateTimeZone.getDefault().id else mOriginalTimeZone val offset = if (!config.allowChangingTimeZones || mEvent.getTimeZoneString().equals(mOriginalTimeZone, true)) {
val millis = System.currentTimeMillis() 0
(DateTimeZone.forID(mEvent.getTimeZoneString()).getOffset(millis) - DateTimeZone.forID(original).getOffset(millis)) / 1000L } else {
} val original = mOriginalTimeZone.ifEmpty { DateTimeZone.getDefault().id }
val millis = System.currentTimeMillis()
val newOffset = DateTimeZone.forID(mEvent.getTimeZoneString()).getOffset(millis)
val oldOffset = DateTimeZone.forID(original).getOffset(millis)
(newOffset - oldOffset) / 1000L
}
val newStartTS = mEventStartDateTime.seconds() - offset val newStartTS = mEventStartDateTime.seconds() - offset
val newEndTS = mEventEndDateTime.seconds() - offset val newEndTS = mEventEndDateTime.seconds() - offset
return Pair(newStartTS, newEndTS) return Pair(newStartTS, newEndTS)
}
} }
private fun getReminders(): ArrayList<Reminder> { private fun getReminders(): ArrayList<Reminder> {
@ -309,6 +310,7 @@ class EventActivity : SimpleActivity() {
mRepeatRule != mEvent.repeatRule || mRepeatRule != mEvent.repeatRule ||
mEventTypeId != mEvent.eventType || mEventTypeId != mEvent.eventType ||
mWasCalendarChanged || mWasCalendarChanged ||
mIsAllDayEvent != mEvent.getIsAllDay() ||
hasTimeChanged hasTimeChanged
) { ) {
return true return true
@ -998,17 +1000,23 @@ class EventActivity : SimpleActivity() {
} }
} }
private fun toggleAllDay(isChecked: Boolean) { private fun toggleAllDay(isAllDay: Boolean) {
mIsAllDayEvent = isChecked
hideKeyboard() hideKeyboard()
event_start_time.beGoneIf(isChecked) mIsAllDayEvent = isAllDay
event_end_time.beGoneIf(isChecked) event_start_time.beGoneIf(isAllDay)
mEvent.timeZone = if (isChecked) DateTimeZone.UTC.id else DateTimeZone.getDefault().id event_end_time.beGoneIf(isAllDay)
updateTimeZoneText() updateTimeZoneText()
showOrHideTimeZone() showOrHideTimeZone()
resetTime() resetTime()
} }
private fun showOrHideTimeZone() {
val allowChangingTimeZones = config.allowChangingTimeZones && !mIsAllDayEvent
event_time_zone_divider.beVisibleIf(allowChangingTimeZones)
event_time_zone_image.beVisibleIf(allowChangingTimeZones)
event_time_zone.beVisibleIf(allowChangingTimeZones)
}
private fun shareEvent() { private fun shareEvent() {
shareEvents(arrayListOf(mEvent.id!!)) shareEvents(arrayListOf(mEvent.id!!))
} }
@ -1157,11 +1165,7 @@ class EventActivity : SimpleActivity() {
reminder3Type = mReminder3Type reminder3Type = mReminder3Type
repeatInterval = mRepeatInterval repeatInterval = mRepeatInterval
importId = newImportId importId = newImportId
timeZone = when { timeZone = if (mIsAllDayEvent || timeZone.isEmpty()) DateTimeZone.getDefault().id else timeZone
mIsAllDayEvent -> DateTimeZone.UTC.id
timeZone.isEmpty() -> DateTimeZone.getDefault().id
else -> timeZone
}
flags = mEvent.flags.addBitIf(event_all_day.isChecked, FLAG_ALL_DAY) flags = mEvent.flags.addBitIf(event_all_day.isChecked, FLAG_ALL_DAY)
repeatLimit = if (repeatInterval == 0) 0 else mRepeatLimit repeatLimit = if (repeatInterval == 0) 0 else mRepeatLimit
repeatRule = mRepeatRule repeatRule = mRepeatRule
@ -1195,7 +1199,7 @@ class EventActivity : SimpleActivity() {
} }
private fun storeEvent(wasRepeatable: Boolean) { private fun storeEvent(wasRepeatable: Boolean) {
if (mEvent.id == null || mEvent.id == null) { if (mEvent.id == null) {
eventsHelper.insertEvent(mEvent, addToCalDAV = true, showToasts = true) { eventsHelper.insertEvent(mEvent, addToCalDAV = true, showToasts = true) {
hideKeyboard() hideKeyboard()

View File

@ -0,0 +1,30 @@
package com.simplemobiletools.calendar.pro.extensions
import com.simplemobiletools.calendar.pro.helpers.Formatter
import com.simplemobiletools.calendar.pro.helpers.TWELVE_HOURS
import com.simplemobiletools.calendar.pro.models.Event
import org.joda.time.DateTimeZone
/** Shifts all-day events to local timezone such that the event starts and ends on the same time as in UTC */
fun Event.toLocalAllDayEvent() {
require(this.getIsAllDay()) { "Must be an all day event!" }
timeZone = DateTimeZone.getDefault().id
startTS = Formatter.getShiftedLocalTS(startTS)
endTS = Formatter.getShiftedLocalTS(endTS)
if (endTS > startTS) {
endTS -= TWELVE_HOURS
}
}
/** Shifts all-day events to UTC such that the event starts on the same time in UTC too */
fun Event.toUtcAllDayEvent() {
require(getIsAllDay()) { "Must be an all day event!" }
if (endTS >= startTS) {
endTS += TWELVE_HOURS
}
timeZone = DateTimeZone.UTC.id
startTS = Formatter.getShiftedUtcTS(startTS)
endTS = Formatter.getShiftedUtcTS(endTS)
}

View File

@ -227,18 +227,14 @@ class CalDAVHelper(val context: Context) {
val repeatRule = Parser().parseRepeatInterval(rrule, startTS) val repeatRule = Parser().parseRepeatInterval(rrule, startTS)
val event = Event( val event = Event(
null, startTS, endTS, title, location, description, reminder1?.minutes ?: REMINDER_OFF, null, startTS, endTS, title, location, description, reminder1?.minutes ?: REMINDER_OFF,
reminder2?.minutes ?: REMINDER_OFF, reminder3?.minutes ?: REMINDER_OFF, reminder1?.type reminder2?.minutes ?: REMINDER_OFF, reminder3?.minutes ?: REMINDER_OFF,
?: REMINDER_NOTIFICATION, reminder2?.type ?: REMINDER_NOTIFICATION, reminder3?.type reminder1?.type ?: REMINDER_NOTIFICATION, reminder2?.type ?: REMINDER_NOTIFICATION,
?: REMINDER_NOTIFICATION, repeatRule.repeatInterval, repeatRule.repeatRule, reminder3?.type ?: REMINDER_NOTIFICATION, repeatRule.repeatInterval, repeatRule.repeatRule,
repeatRule.repeatLimit, ArrayList(), attendees, importId, eventTimeZone, allDay, eventTypeId, source = source, availability = availability repeatRule.repeatLimit, ArrayList(), attendees, importId, eventTimeZone, allDay, eventTypeId, source = source, availability = availability
) )
if (event.getIsAllDay()) { if (event.getIsAllDay()) {
event.startTS = Formatter.getShiftedImportTimestamp(event.startTS) event.toLocalAllDayEvent()
event.endTS = Formatter.getShiftedImportTimestamp(event.endTS)
if (event.endTS > event.startTS) {
event.endTS -= DAY
}
} }
fetchedEventIds.add(importId) fetchedEventIds.add(importId)
@ -402,9 +398,6 @@ class CalDAVHelper(val context: Context) {
put(Events.CALENDAR_ID, event.getCalDAVCalendarId()) put(Events.CALENDAR_ID, event.getCalDAVCalendarId())
put(Events.TITLE, event.title) put(Events.TITLE, event.title)
put(Events.DESCRIPTION, event.description) put(Events.DESCRIPTION, event.description)
put(Events.DTSTART, event.startTS * 1000L)
put(Events.ALL_DAY, if (event.getIsAllDay()) 1 else 0)
put(Events.EVENT_TIMEZONE, event.getTimeZoneString())
put(Events.EVENT_LOCATION, event.location) put(Events.EVENT_LOCATION, event.location)
put(Events.STATUS, Events.STATUS_CONFIRMED) put(Events.STATUS, Events.STATUS_CONFIRMED)
put(Events.AVAILABILITY, event.availability) put(Events.AVAILABILITY, event.availability)
@ -416,9 +409,15 @@ class CalDAVHelper(val context: Context) {
put(Events.RRULE, repeatRule) put(Events.RRULE, repeatRule)
} }
if (event.getIsAllDay() && event.endTS >= event.startTS) if (event.getIsAllDay()) {
event.endTS += DAY event.toUtcAllDayEvent()
put(Events.ALL_DAY, 1)
} else {
put(Events.ALL_DAY, 0)
}
put(Events.DTSTART, event.startTS * 1000L)
put(Events.EVENT_TIMEZONE, event.getTimeZoneString())
if (event.repeatInterval > 0) { if (event.repeatInterval > 0) {
put(Events.DURATION, getDurationCode(event)) put(Events.DURATION, getDurationCode(event))
putNull(Events.DTEND) putNull(Events.DTEND)

View File

@ -53,6 +53,7 @@ const val DEFAULT_START_TIME_CURRENT_TIME = -2
const val TYPE_EVENT = 0 const val TYPE_EVENT = 0
const val TYPE_TASK = 1 const val TYPE_TASK = 1
const val TWELVE_HOURS = 43200
const val DAY = 86400 const val DAY = 86400
const val WEEK = 604800 const val WEEK = 604800
const val MONTH = 2592001 // exact value not taken into account, Joda is used for adding months and years const val MONTH = 2592001 // exact value not taken into account, Joda is used for adding months and years

View File

@ -135,7 +135,11 @@ object Formatter {
fun getUTCDayCodeFromTS(ts: Long) = getUTCDateTimeFromTS(ts).toString(DAYCODE_PATTERN) fun getUTCDayCodeFromTS(ts: Long) = getUTCDateTimeFromTS(ts).toString(DAYCODE_PATTERN)
fun getShiftedImportTimestamp(ts: Long) = getUTCDateTimeFromTS(ts).withTime(13, 0, 0, 0).withZoneRetainFields(DateTimeZone.getDefault()).seconds()
fun getYearFromDayCode(dayCode: String) = getDateTimeFromCode(dayCode).toString(YEAR_PATTERN) fun getYearFromDayCode(dayCode: String) = getDateTimeFromCode(dayCode).toString(YEAR_PATTERN)
fun getShiftedTS(dateTime: DateTime, toZone: DateTimeZone) = dateTime.withTimeAtStartOfDay().withZoneRetainFields(toZone).seconds()
fun getShiftedLocalTS(ts: Long) = getShiftedTS(dateTime = getUTCDateTimeFromTS(ts), toZone = DateTimeZone.getDefault())
fun getShiftedUtcTS(ts: Long) = getShiftedTS(dateTime = getDateTimeFromTS(ts), toZone = DateTimeZone.UTC)
} }

View File

@ -4,7 +4,9 @@ import android.provider.CalendarContract.Events
import com.simplemobiletools.calendar.pro.R import com.simplemobiletools.calendar.pro.R
import com.simplemobiletools.calendar.pro.extensions.calDAVHelper import com.simplemobiletools.calendar.pro.extensions.calDAVHelper
import com.simplemobiletools.calendar.pro.extensions.eventTypesDB import com.simplemobiletools.calendar.pro.extensions.eventTypesDB
import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.* import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.EXPORT_FAIL
import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.EXPORT_OK
import com.simplemobiletools.calendar.pro.helpers.IcsExporter.ExportResult.EXPORT_PARTIAL
import com.simplemobiletools.calendar.pro.models.CalDAVCalendar import com.simplemobiletools.calendar.pro.models.CalDAVCalendar
import com.simplemobiletools.calendar.pro.models.Event import com.simplemobiletools.calendar.pro.models.Event
import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.activities.BaseSimpleActivity
@ -61,7 +63,7 @@ class IcsExporter {
if (event.getIsAllDay()) { if (event.getIsAllDay()) {
out.writeLn("$DTSTART;$VALUE=$DATE:${Formatter.getDayCodeFromTS(event.startTS)}") out.writeLn("$DTSTART;$VALUE=$DATE:${Formatter.getDayCodeFromTS(event.startTS)}")
out.writeLn("$DTEND;$VALUE=$DATE:${Formatter.getDayCodeFromTS(event.endTS + DAY)}") out.writeLn("$DTEND;$VALUE=$DATE:${Formatter.getDayCodeFromTS(event.endTS + TWELVE_HOURS)}")
} else { } else {
event.startTS.let { out.writeLn("$DTSTART:${Formatter.getExportedTime(it * 1000L)}") } event.startTS.let { out.writeLn("$DTSTART:${Formatter.getExportedTime(it * 1000L)}") }
event.endTS.let { out.writeLn("$DTEND:${Formatter.getExportedTime(it * 1000L)}") } event.endTS.let { out.writeLn("$DTEND:${Formatter.getExportedTime(it * 1000L)}") }

View File

@ -232,7 +232,7 @@ class IcsImporter(val activity: SimpleActivity) {
curRepeatExceptions, curRepeatExceptions,
"", "",
curImportId, curImportId,
if (isAllDay) DateTimeZone.UTC.id else DateTimeZone.getDefault().id, DateTimeZone.getDefault().id,
curFlags, curFlags,
curEventTypeId, curEventTypeId,
0, 0,
@ -242,8 +242,7 @@ class IcsImporter(val activity: SimpleActivity) {
) )
if (isAllDay && curEnd > curStart) { if (isAllDay && curEnd > curStart) {
event.endTS -= DAY event.endTS -= TWELVE_HOURS
// fix some glitches related to daylight saving shifts // fix some glitches related to daylight saving shifts
if (event.startTS - event.endTS == HOUR_SECONDS.toLong()) { if (event.startTS - event.endTS == HOUR_SECONDS.toLong()) {
event.endTS += HOUR_SECONDS event.endTS += HOUR_SECONDS

View File

@ -10,6 +10,7 @@ import com.simplemobiletools.commons.extensions.areDigitsOnly
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import org.joda.time.DateTimeZone import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormat
import kotlin.math.floor
class Parser { class Parser {
// from RRULE:FREQ=DAILY;COUNT=5 to Daily, 5x... // from RRULE:FREQ=DAILY;COUNT=5 to Daily, 5x...
@ -101,8 +102,9 @@ class Parser {
return if (edited.length == 14) { return if (edited.length == 14) {
parseLongFormat(edited, value.endsWith("Z")) parseLongFormat(edited, value.endsWith("Z"))
} else { } else {
val dateTimeFormat = DateTimeFormat.forPattern("yyyyMMdd") val dateTimeFormat = DateTimeFormat.forPattern("yyyyMMdd").withZoneUTC()
dateTimeFormat.parseDateTime(edited).withHourOfDay(13).seconds() val dateTime = dateTimeFormat.parseDateTime(edited)
Formatter.getShiftedTS(dateTime = dateTime, toZone = DateTimeZone.getDefault())
} }
} }
@ -227,12 +229,12 @@ class Parser {
var hours = 0 var hours = 0
var remainder = minutes var remainder = minutes
if (remainder >= DAY_MINUTES) { if (remainder >= DAY_MINUTES) {
days = Math.floor((remainder / DAY_MINUTES).toDouble()).toInt() days = floor((remainder / DAY_MINUTES).toDouble()).toInt()
remainder -= days * DAY_MINUTES remainder -= days * DAY_MINUTES
} }
if (remainder >= 60) { if (remainder >= 60) {
hours = Math.floor((remainder / 60).toDouble()).toInt() hours = floor((remainder / 60).toDouble()).toInt()
remainder -= hours * 60 remainder -= hours * 60
} }