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_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_rule_holder.setOnClickListener { showRepetitionRuleDialog() }
event_repetition_limit_holder.setOnClickListener { showRepetitionTypePicker() }
@ -213,7 +213,7 @@ class EventActivity : SimpleActivity() {
event_type_holder.setOnClickListener { showEventTypeDialog() }
event_all_day.apply {
isChecked = mEvent.flags and FLAG_ALL_DAY != 0
isChecked = mEvent.getIsAllDay()
jumpDrawablesToCurrentState()
}
@ -227,13 +227,6 @@ class EventActivity : SimpleActivity() {
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() {
if (::mEvent.isInitialized) {
event_toolbar.menu.apply {
@ -258,17 +251,25 @@ class EventActivity : SimpleActivity() {
}
private fun getStartEndTimes(): Pair<Long, Long> {
val offset = if (!config.allowChangingTimeZones || mEvent.getTimeZoneString().equals(mOriginalTimeZone, true)) {
0
if (mIsAllDayEvent) {
val newStartTS = mEventStartDateTime.withTimeAtStartOfDay().seconds()
val newEndTS = mEventEndDateTime.withTimeAtStartOfDay().withHourOfDay(12).seconds()
return Pair(newStartTS, newEndTS)
} else {
val original = if (mOriginalTimeZone.isEmpty()) DateTimeZone.getDefault().id else mOriginalTimeZone
val millis = System.currentTimeMillis()
(DateTimeZone.forID(mEvent.getTimeZoneString()).getOffset(millis) - DateTimeZone.forID(original).getOffset(millis)) / 1000L
}
val offset = if (!config.allowChangingTimeZones || mEvent.getTimeZoneString().equals(mOriginalTimeZone, true)) {
0
} 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 newEndTS = mEventEndDateTime.seconds() - offset
return Pair(newStartTS, newEndTS)
val newStartTS = mEventStartDateTime.seconds() - offset
val newEndTS = mEventEndDateTime.seconds() - offset
return Pair(newStartTS, newEndTS)
}
}
private fun getReminders(): ArrayList<Reminder> {
@ -309,6 +310,7 @@ class EventActivity : SimpleActivity() {
mRepeatRule != mEvent.repeatRule ||
mEventTypeId != mEvent.eventType ||
mWasCalendarChanged ||
mIsAllDayEvent != mEvent.getIsAllDay() ||
hasTimeChanged
) {
return true
@ -998,17 +1000,23 @@ class EventActivity : SimpleActivity() {
}
}
private fun toggleAllDay(isChecked: Boolean) {
mIsAllDayEvent = isChecked
private fun toggleAllDay(isAllDay: Boolean) {
hideKeyboard()
event_start_time.beGoneIf(isChecked)
event_end_time.beGoneIf(isChecked)
mEvent.timeZone = if (isChecked) DateTimeZone.UTC.id else DateTimeZone.getDefault().id
mIsAllDayEvent = isAllDay
event_start_time.beGoneIf(isAllDay)
event_end_time.beGoneIf(isAllDay)
updateTimeZoneText()
showOrHideTimeZone()
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() {
shareEvents(arrayListOf(mEvent.id!!))
}
@ -1157,11 +1165,7 @@ class EventActivity : SimpleActivity() {
reminder3Type = mReminder3Type
repeatInterval = mRepeatInterval
importId = newImportId
timeZone = when {
mIsAllDayEvent -> DateTimeZone.UTC.id
timeZone.isEmpty() -> DateTimeZone.getDefault().id
else -> timeZone
}
timeZone = if (mIsAllDayEvent || timeZone.isEmpty()) DateTimeZone.getDefault().id else timeZone
flags = mEvent.flags.addBitIf(event_all_day.isChecked, FLAG_ALL_DAY)
repeatLimit = if (repeatInterval == 0) 0 else mRepeatLimit
repeatRule = mRepeatRule
@ -1195,7 +1199,7 @@ class EventActivity : SimpleActivity() {
}
private fun storeEvent(wasRepeatable: Boolean) {
if (mEvent.id == null || mEvent.id == null) {
if (mEvent.id == null) {
eventsHelper.insertEvent(mEvent, addToCalDAV = true, showToasts = true) {
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 event = Event(
null, startTS, endTS, title, location, description, reminder1?.minutes ?: REMINDER_OFF,
reminder2?.minutes ?: REMINDER_OFF, reminder3?.minutes ?: REMINDER_OFF, reminder1?.type
?: REMINDER_NOTIFICATION, reminder2?.type ?: REMINDER_NOTIFICATION, reminder3?.type
?: REMINDER_NOTIFICATION, repeatRule.repeatInterval, repeatRule.repeatRule,
reminder2?.minutes ?: REMINDER_OFF, reminder3?.minutes ?: REMINDER_OFF,
reminder1?.type ?: REMINDER_NOTIFICATION, reminder2?.type ?: REMINDER_NOTIFICATION,
reminder3?.type ?: REMINDER_NOTIFICATION, repeatRule.repeatInterval, repeatRule.repeatRule,
repeatRule.repeatLimit, ArrayList(), attendees, importId, eventTimeZone, allDay, eventTypeId, source = source, availability = availability
)
if (event.getIsAllDay()) {
event.startTS = Formatter.getShiftedImportTimestamp(event.startTS)
event.endTS = Formatter.getShiftedImportTimestamp(event.endTS)
if (event.endTS > event.startTS) {
event.endTS -= DAY
}
event.toLocalAllDayEvent()
}
fetchedEventIds.add(importId)
@ -402,9 +398,6 @@ class CalDAVHelper(val context: Context) {
put(Events.CALENDAR_ID, event.getCalDAVCalendarId())
put(Events.TITLE, event.title)
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.STATUS, Events.STATUS_CONFIRMED)
put(Events.AVAILABILITY, event.availability)
@ -416,9 +409,15 @@ class CalDAVHelper(val context: Context) {
put(Events.RRULE, repeatRule)
}
if (event.getIsAllDay() && event.endTS >= event.startTS)
event.endTS += DAY
if (event.getIsAllDay()) {
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) {
put(Events.DURATION, getDurationCode(event))
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_TASK = 1
const val TWELVE_HOURS = 43200
const val DAY = 86400
const val WEEK = 604800
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 getShiftedImportTimestamp(ts: Long) = getUTCDateTimeFromTS(ts).withTime(13, 0, 0, 0).withZoneRetainFields(DateTimeZone.getDefault()).seconds()
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.extensions.calDAVHelper
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.Event
import com.simplemobiletools.commons.activities.BaseSimpleActivity
@ -61,7 +63,7 @@ class IcsExporter {
if (event.getIsAllDay()) {
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 {
event.startTS.let { out.writeLn("$DTSTART:${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,
"",
curImportId,
if (isAllDay) DateTimeZone.UTC.id else DateTimeZone.getDefault().id,
DateTimeZone.getDefault().id,
curFlags,
curEventTypeId,
0,
@ -242,8 +242,7 @@ class IcsImporter(val activity: SimpleActivity) {
)
if (isAllDay && curEnd > curStart) {
event.endTS -= DAY
event.endTS -= TWELVE_HOURS
// fix some glitches related to daylight saving shifts
if (event.startTS - event.endTS == HOUR_SECONDS.toLong()) {
event.endTS += HOUR_SECONDS

View File

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