Handle timezone when calculating start of day

main
Alex Baker 2 days ago
parent 46412a92c4
commit ef0c1ac981

@ -0,0 +1,142 @@
package com.todoroo.astrid.gcal
import android.Manifest
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.tasks.TestUtilities.withTZ
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidTest
class GCalHelperTest : InjectingTestCase() {
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
@Inject lateinit var gcalHelper: GCalHelper
private var testCalendarId: Long = -1
@Before
override fun setUp() {
super.setUp()
testCalendarId = createTestCalendar()
}
@After
fun tearDown() {
if (testCalendarId > 0) {
try {
val context = ApplicationProvider.getApplicationContext<Context>()
context.contentResolver.delete(
ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendarId),
null,
null
)
} catch (e: Exception) {
Timber.e(e)
}
}
}
@Test fun allDayEventInNewYork() = assertAllDayEvent("America/New_York") // UTC-5
@Test fun allDayEventInBerlin() = assertAllDayEvent("Europe/Berlin") // UTC+1
@Test fun allDayEventInAuckland() = assertAllDayEvent("Pacific/Auckland") // UTC+13
@Test fun allDayEventInTokyo() = assertAllDayEvent("Asia/Tokyo") // UTC+9
@Test fun allDayEventInHonolulu() = assertAllDayEvent("Pacific/Honolulu") // UTC-10
@Test fun allDayEventInChatham() = assertAllDayEvent("Pacific/Chatham") // UTC+13:45
private fun assertAllDayEvent(timezone: String) = withTZ(timezone) {
val task = Task(dueDate = DateTime(2024, 12, 20).millis)
val eventUri = gcalHelper.createTaskEvent(task, testCalendarId.toString())
?: throw RuntimeException("Event not created")
val event = queryEvent(eventUri.toString()) ?: throw RuntimeException("Event not found")
assertEquals(
"DTSTART should be Dec 20 00:00 UTC",
DateTime(2024, 12, 20, timeZone = DateTime.UTC).millis,
event.dtStart
)
assertEquals(
"DTEND should be Dec 21 00:00 UTC",
DateTime(2024, 12, 21, timeZone = DateTime.UTC).millis,
event.dtEnd
)
}
private fun createTestCalendar(): Long {
val context = ApplicationProvider.getApplicationContext<Context>()
val values = ContentValues().apply {
put(Calendars.ACCOUNT_NAME, "test@test.com")
put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
put(Calendars.NAME, "Test Calendar")
put(Calendars.CALENDAR_DISPLAY_NAME, "Test Calendar")
put(Calendars.CALENDAR_COLOR, 0xFF0000)
put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
put(Calendars.OWNER_ACCOUNT, "test@test.com")
put(Calendars.VISIBLE, 1)
put(Calendars.SYNC_EVENTS, 1)
}
val uri = Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "test@test.com")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build()
val calendarUri = context.contentResolver.insert(uri, values)
return ContentUris.parseId(calendarUri!!)
}
private fun queryEvent(eventUri: String): CalendarEvent? {
val context = ApplicationProvider.getApplicationContext<Context>()
val cursor = context.contentResolver.query(
eventUri.toUri(),
arrayOf(
Events.DTSTART,
Events.DTEND,
Events.ALL_DAY,
Events.EVENT_TIMEZONE
),
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
CalendarEvent(
dtStart = it.getLong(0),
dtEnd = it.getLong(1),
allDay = it.getInt(2) == 1,
timezone = it.getString(3)
)
} else null
}
}
private data class CalendarEvent(
val dtStart: Long,
val dtEnd: Long,
val allDay: Boolean,
val timezone: String?
)
}

@ -151,7 +151,7 @@ class GCalHelper @Inject constructor(
values.put(CalendarContract.Events.ALL_DAY, "0")
values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
} else {
val utcMidnight = DateTime(dueDate).toUTC().startOfDay()
val utcMidnight = DateTime(dueDate).startOfDay(DateTime.UTC)
values.put(CalendarContract.Events.DTSTART, utcMidnight.millis)
values.put(CalendarContract.Events.DTEND, utcMidnight.plusDays(1).millis)
values.put(CalendarContract.Events.ALL_DAY, "1")

@ -44,7 +44,13 @@ class DateTime {
private constructor(calendar: Calendar) : this(calendar.timeInMillis, calendar.timeZone)
fun startOfDay(): DateTime = DateTime(millis.startOfDay())
@JvmOverloads
fun startOfDay(timeZone: TimeZone = this.timeZone): DateTime = DateTime(
year = year,
month = monthOfYear,
day = dayOfMonth,
timeZone = timeZone
)
fun startOfMinute(): DateTime = DateTime(millis.startOfMinute())

@ -1,9 +1,12 @@
package org.tasks.time
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.Freeze
import org.tasks.TestUtilities.withTZ
import java.util.TimeZone
import java.util.concurrent.TimeUnit
class DateTimeTest {
@ -354,4 +357,91 @@ class DateTimeTest {
DateTime(2017, 9, 22, 14, 47, 59, 999),
DateTime(2017, 9, 22, 14, 47, 14, 453).endOfMinute())
}
}
@Test
fun startOfDayPreservesTimezone() {
val utcDateTime = DateTime(2024, 12, 20, 14, 30, timeZone = DateTime.UTC)
val result = utcDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), result)
}
@Test
fun startOfDayInDefaultTimezone() {
val dateTime = DateTime(2024, 12, 20, 14, 30)
val result = dateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
@Test
fun startOfDayWithUTCTimezone() {
withTZ("America/Chicago") { // UTC-6
val utcDateTime = DateTime(2024, 12, 20, 15, timeZone = DateTime.UTC)
val result = utcDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), result)
}
}
@Test
fun startOfDayBeforeUTC() {
withTZ("America/New_York") { // UTC-5
val nyDateTime = DateTime(2024, 12, 20, 15)
val result = nyDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayAfterUTC() {
withTZ("Europe/Berlin") { // UTC+1
val berlinDateTime = DateTime(2024, 12, 20, 15)
val result = berlinDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayWithDateBoundaryWrap() {
withTZ("Pacific/Auckland") { // UTC+13
val aucklandDateTime = DateTime(2024, 12, 20, 12)
val result = aucklandDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20), result)
}
}
@Test
fun startOfDayRespectsTimezoneNotSystemDefault() {
withTZ("America/New_York") { // UTC-5
val berlinTz = TimeZone.getTimeZone("Europe/Berlin") // UTC+1
val berlinDateTime = DateTime(2024, 12, 20, 1, timeZone = berlinTz)
val result = berlinDateTime.startOfDay()
assertEquals(DateTime(2024, 12, 20, timeZone = berlinTz), result)
}
}
@Test
fun startOfDayWithExplicitTimezone() {
withTZ("Europe/Berlin") { // UTC+1
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
@Test
fun startOfDayWithExplicitTimezoneFromAuckland() {
withTZ("Pacific/Auckland") { // UTC+13
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
@Test
fun startOfDayWithExplicitTimezoneFromHonolulu() {
withTZ("Pacific/Honolulu") { // UTC-10
val localMidnight = DateTime(2024, 12, 20)
val utcMidnight = localMidnight.startOfDay(DateTime.UTC)
assertEquals(DateTime(2024, 12, 20, timeZone = DateTime.UTC), utcMidnight)
}
}
}

Loading…
Cancel
Save