diff --git a/app/src/main/java/org/tasks/sync/microsoft/Error.kt b/app/src/main/java/org/tasks/sync/microsoft/Error.kt index 7f5b241c7..7d676b514 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/Error.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/Error.kt @@ -15,6 +15,10 @@ data class Error( ) companion object { - suspend fun HttpResponse.toMicrosoftError() = body() + suspend fun HttpResponse.toMicrosoftError(): Error? = try { + body() + } catch (_: Exception) { + null + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt index 9e83e3512..495c8eebf 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftConverter.kt @@ -4,8 +4,10 @@ import org.tasks.data.entity.Task import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.WeekDay import net.fortuna.ical4j.model.WeekDayList +import org.tasks.data.createDueDate import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.TagData +import org.tasks.date.DateTimeUtils import org.tasks.sync.microsoft.Tasks.Task.RecurrenceDayOfWeek import org.tasks.sync.microsoft.Tasks.Task.RecurrenceType import org.tasks.time.DateTime @@ -34,7 +36,20 @@ object MicrosoftConverter { else -> Task.Priority.NONE } completionDate = remote.completedDateTime.toLong(currentTimeMillis()) - dueDate = remote.dueDateTime.toLong(0L) + remote.dueDateTime.toLong(0L).let { + if (it > 0 && hasDueTime()) { + val oldDate = DateTimeUtils.newDateTime(dueDate) + val newDate = DateTimeUtils.newDateTime(it) + .withHourOfDay(oldDate.hourOfDay) + .withMinuteOfHour(oldDate.minuteOfHour) + .withSecondOfMinute(oldDate.secondOfMinute) + setDueDateAdjustingHideUntil( + createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDate.millis) + ) + } else { + setDueDateAdjustingHideUntil(it) + } + } creationDate = remote.createdDateTime.parseDateTime() modificationDate = remote.lastModifiedDateTime.parseDateTime() recurrence = remote.recurrence?.let { recurrence -> @@ -92,13 +107,13 @@ object MicrosoftConverter { categories = tags.map { it.name!! }.takeIf { it.isNotEmpty() }, dueDateTime = if (hasDueDate()) { Tasks.Task.DateTime( - dateTime = DateTime(dueDate).startOfDay().toUTC().toString(DATE_TIME_FORMAT), - timeZone = "UTC" + dateTime = DateTime(dueDate).startOfDay().toString(DATE_TIME_FORMAT), + timeZone = TimeZone.getDefault().id ) - } else if (isRecurring) { + } else if (isRecurring) { // fallback: recurring task must have due date Tasks.Task.DateTime( - dateTime = DateTime().startOfDay().toUTC().toString(DATE_TIME_FORMAT), - timeZone = "UTC" + dateTime = DateTime().startOfDay().toString(DATE_TIME_FORMAT), + timeZone = TimeZone.getDefault().id ) } else { null @@ -107,8 +122,8 @@ object MicrosoftConverter { createdDateTime = DateTime(creationDate).toUTC().toString(DATE_TIME_UTC_FORMAT), completedDateTime = if (isCompleted) { Tasks.Task.DateTime( - dateTime = DateTime(completionDate).toUTC().toString(DATE_TIME_FORMAT), - timeZone = "UTC", + dateTime = DateTime(completionDate).toString(DATE_TIME_FORMAT), + timeZone = TimeZone.getDefault().id, ) } else { null @@ -168,13 +183,13 @@ object MicrosoftConverter { private fun Tasks.Task.DateTime?.toLong(default: Long): Long = this - ?.let { - val tz = TimeZone.getTimeZone(it.timeZone) + ?.let { task -> + val tz = TimeZone.getTimeZone(task.timeZone) SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.ssssss", Locale.US) .apply { timeZone = tz } - .parse(it.dateTime) + .parse(task.dateTime) ?.time - ?.let { ts -> DateTime(ts, tz).toLocal().millis } + ?.let { DateTime(it, tz).millis } ?: default } ?: 0L diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftService.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftService.kt index 9ccf8483b..182082340 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftService.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftService.kt @@ -37,10 +37,9 @@ class MicrosoftService( suspend fun deleteList(listId: String) = client.delete("$baseUrl/lists/$listId") - suspend fun getTasks(listId: String): Tasks = - client.get("$baseUrl/lists/$listId/tasks/delta").body() + suspend fun getTasks(listId: String) = client.get("$baseUrl/lists/$listId/tasks/delta") - suspend fun paginateTasks(nextPage: String): Tasks = client.get(nextPage).body() + suspend fun paginateTasks(nextPage: String) = client.get(nextPage) suspend fun createTask(listId: String, body: Tasks.Task): Tasks.Task = client diff --git a/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt b/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt index 711750d8b..25daa2049 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/TaskLists.kt @@ -9,7 +9,7 @@ import org.tasks.data.entity.CaldavCalendar data class TaskLists( @SerialName("@odata.context") val context: String, val value: List, - @SerialName("@odata.nextLink") val nextPage: String?, + @SerialName("@odata.nextLink") val nextPage: String? = null, ) { @Serializable data class TaskList( diff --git a/app/src/main/java/org/tasks/sync/microsoft/Tasks.kt b/app/src/main/java/org/tasks/sync/microsoft/Tasks.kt index bb0338f25..b0da29272 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/Tasks.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/Tasks.kt @@ -26,6 +26,8 @@ data class Tasks( val dueDateTime: DateTime? = null, val linkedResources: List? = null, val recurrence: Recurrence? = null, + val reminderDateTime: DateTime? = null, + val checklistItems: List? = null, @SerialName("@removed") val removed: Removed? = null, ) { @Serializable @@ -96,6 +98,14 @@ data class Tasks( saturday, } + @Serializable + data class ChecklistItem( + val id: String, + val displayName: String, + val createdDateTime: String, + val isChecked: Boolean, + ) + enum class Importance { low, normal, diff --git a/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt b/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt index 9900f2538..55de1044a 100644 --- a/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt +++ b/app/src/test/java/org/tasks/sync/microsoft/ConvertFromMicrosoftTests.kt @@ -61,24 +61,24 @@ class ConvertFromMicrosoftTests { @Test fun parseCompletionDate() { - val (local, _) = TestUtilities.mstodo("microsoft/completed_task.txt") withTZ("America/Chicago") { + val (local, _) = TestUtilities.mstodo("microsoft/completed_task.txt") assertEquals(DateTime(2022, 9, 18, 0, 0).millis, local.completionDate) } } @Test fun parseDueDate() { - val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") withTZ("America/Chicago") { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") assertEquals(DateTime(2023, 7, 19, 0, 0).millis, local.dueDate) } } @Test fun parseCreationDate() { - val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") withTZ("America/Chicago") { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") assertEquals( DateTime(2023, 7, 19, 23, 20, 56, 9).millis, local.creationDate @@ -88,8 +88,8 @@ class ConvertFromMicrosoftTests { @Test fun parseModificationDate() { - val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") withTZ("America/Chicago") { + val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt") assertEquals( DateTime(2023, 7, 19, 23, 21, 6, 269).millis, local.modificationDate diff --git a/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt b/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt index 60e57f1f2..af6524bc3 100644 --- a/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt +++ b/app/src/test/java/org/tasks/sync/microsoft/ConvertToMicrosoftTests.kt @@ -139,8 +139,8 @@ class ConvertToMicrosoftTests { with(DUE_TIME, DateTime(2023, 7, 21, 13, 30)) ) .toRemote() - assertEquals("2023-07-21T05:00:00.0000000", remote.dueDateTime?.dateTime) - assertEquals("UTC", remote.dueDateTime?.timeZone) + assertEquals("2023-07-21T00:00:00.0000000", remote.dueDateTime?.dateTime) + assertEquals("America/Chicago", remote.dueDateTime?.timeZone) } } diff --git a/app/src/test/resources/microsoft/task_with_reminder.txt b/app/src/test/resources/microsoft/task_with_reminder.txt new file mode 100644 index 000000000..dd8f5aa34 --- /dev/null +++ b/app/src/test/resources/microsoft/task_with_reminder.txt @@ -0,0 +1,27 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)", + "@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99ngHvnDU_spnidrOYzb78--8L_CvTDIAkbXl6MuXT-elfKFgB_iu9jwMdUnbCSooCAPAbUSRcpefYeIuTrt_20ZA.4hYl5mAJ-NfssRdizzms-3tZN2OLFUtndPb1x3Ca9Ak", + "value": [ + { + "@odata.type": "#microsoft.graph.todoTask", + "@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGX5rVEA==\"", + "importance": "normal", + "isReminderOn": true, + "status": "notStarted", + "title": "Test", + "createdDateTime": "2023-07-22T04:58:52.9064041Z", + "lastModifiedDateTime": "2023-07-22T04:58:52.9358461Z", + "hasAttachments": false, + "categories": [], + "id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABl95iCgAAAA=", + "body": { + "content": "", + "contentType": "text" + }, + "reminderDateTime": { + "dateTime": "2023-07-22T14:00:00.0000000", + "timeZone": "UTC" + } + } + ] +} \ No newline at end of file diff --git a/app/src/test/resources/microsoft/task_with_subtasks.txt b/app/src/test/resources/microsoft/task_with_subtasks.txt new file mode 100644 index 000000000..7e8393aed --- /dev/null +++ b/app/src/test/resources/microsoft/task_with_subtasks.txt @@ -0,0 +1,43 @@ +{ + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)", + "@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99nvA6Vw_JoxmsCI684Nwl-4ciS2_UHNuV2sSv4_GUGLu08H56FKa7bq_dusmAUGzg0npiBEHMJiTtGq4Tydf9hKM.uyzmw12DAcVivLt2W2wnOpvqSqoOwLQK4lxe_EG5Tzo", + "value": [ + { + "@odata.type": "#microsoft.graph.todoTask", + "@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGhj1xBA==\"", + "importance": "normal", + "isReminderOn": false, + "status": "notStarted", + "title": "Test with subtasks", + "createdDateTime": "2023-09-18T04:43:02.7761097Z", + "lastModifiedDateTime": "2023-09-18T04:43:12.7208067Z", + "hasAttachments": false, + "categories": [], + "id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABoYcWyYAAAA=", + "body": { + "content": "", + "contentType": "text" + }, + "checklistItems": [ + { + "displayName": "A", + "createdDateTime": "2023-09-18T04:43:10.1170114Z", + "isChecked": false, + "id": "426d6b9f-6cb0-4642-b9ed-b12a17fa0730" + }, + { + "displayName": "B", + "createdDateTime": "2023-09-18T04:43:10.6820549Z", + "isChecked": false, + "id": "774adbcb-5e89-4e1a-896d-6b18f898d6e0" + }, + { + "displayName": "C", + "createdDateTime": "2023-09-18T04:43:12.6585325Z", + "isChecked": false, + "id": "ecde7824-1e4a-42b2-b3a8-aea7504d316e" + } + ] + } + ] +} \ No newline at end of file