/* * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.gcal import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.net.Uri import android.provider.CalendarContract import android.text.format.Time import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.data.Task import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.R import org.tasks.Strings.isNullOrEmpty import org.tasks.calendars.CalendarEventProvider import org.tasks.data.TaskDao import org.tasks.preferences.PermissionChecker import org.tasks.preferences.Preferences import timber.log.Timber import java.util.* import javax.inject.Inject class GCalHelper @Inject constructor( @ApplicationContext private val context: Context, private val taskDao: TaskDao, private val preferences: Preferences, private val permissionChecker: PermissionChecker, private val calendarEventProvider: CalendarEventProvider) { private val cr: ContentResolver = context.contentResolver private suspend fun getTaskEventUri(task: Task) = if (!task.calendarURI.isNullOrBlank()) { task.calendarURI } else { taskDao.fetch(task.id) ?.calendarURI } suspend fun createTaskEventIfEnabled(t: Task) { if (!t.hasDueDate()) { return } createTaskEventIfEnabled(t, true) } private suspend fun createTaskEventIfEnabled(t: Task, deleteEventIfExists: Boolean) { if (preferences.isDefaultCalendarSet) { val calendarUri = createTaskEvent(t, ContentValues(), deleteEventIfExists) if (calendarUri != null) { t.calendarURI = calendarUri.toString() } } } suspend fun createTaskEvent(task: Task, calendarId: String?): Uri? { val values = ContentValues() values.put(CalendarContract.Events.CALENDAR_ID, calendarId) return createTaskEvent(task, values, true) } private suspend fun createTaskEvent(task: Task, values: ContentValues, deleteEventIfExists: Boolean): Uri? { if (!permissionChecker.canAccessCalendars()) { return null } val eventuri = getTaskEventUri(task) if (!isNullOrEmpty(eventuri) && deleteEventIfExists) { calendarEventProvider.deleteEvent(task) } try { values.put(CalendarContract.Events.TITLE, task.title) values.put(CalendarContract.Events.DESCRIPTION, task.notes) values.put(CalendarContract.Events.HAS_ALARM, 0) val valuesContainCalendarId = (values.containsKey(CalendarContract.Events.CALENDAR_ID) && !isNullOrEmpty(values.getAsString(CalendarContract.Events.CALENDAR_ID))) if (!valuesContainCalendarId) { val calendarId = preferences.defaultCalendar if (!isNullOrEmpty(calendarId)) { values.put(CalendarContract.Events.CALENDAR_ID, calendarId) } } createStartAndEndDate(task, values) val eventUri = cr.insert(CalendarContract.Events.CONTENT_URI, values) cr.notifyChange(eventUri!!, null) return eventUri } catch (e: Exception) { // won't work on emulator Timber.e(e) } return null } fun updateEvent(task: Task) { val uri = task.calendarURI?.takeIf { it.isNotBlank() } ?: return if (!permissionChecker.canAccessCalendars()) { return } try { val updateValues = ContentValues() updateValues.put(CalendarContract.Events.TITLE, if (task.isCompleted) { context.getString(R.string.gcal_completed_title, task.title) } else { task.title }) updateValues.put(CalendarContract.Events.DESCRIPTION, task.notes) createStartAndEndDate(task, updateValues) cr.update(Uri.parse(uri), updateValues, null, null) } catch (e: Exception) { Timber.e(e, "Failed to update calendar: %s [%s]", uri, task) } } suspend fun rescheduleRepeatingTask(task: Task) { val taskUri = getTaskEventUri(task) if (isNullOrEmpty(taskUri)) { return } val eventUri = Uri.parse(taskUri) val event = calendarEventProvider.getEvent(eventUri) if (event == null) { task.calendarURI = "" return } val cv = ContentValues() cv.put(CalendarContract.Events.CALENDAR_ID, event.calendarId) val uri = createTaskEvent(task, cv, false) task.calendarURI = uri.toString() } private fun createStartAndEndDate(task: Task, values: ContentValues) { val dueDate = task.dueDate val tzCorrectedDueDate = dueDate + TimeZone.getDefault().getOffset(dueDate) val tzCorrectedDueDateNow = DateUtilities.now() + TimeZone.getDefault().getOffset(DateUtilities.now()) // FIXME: doesnt respect timezones, see story 17443653 if (task.hasDueDate()) { if (task.hasDueTime()) { var estimatedTime = task.estimatedSeconds * 1000.toLong() if (estimatedTime <= 0) { estimatedTime = DEFAULT_CAL_TIME } if (preferences.getBoolean(R.string.p_end_at_deadline, true)) { values.put(CalendarContract.Events.DTSTART, dueDate) values.put(CalendarContract.Events.DTEND, dueDate + estimatedTime) } else { values.put(CalendarContract.Events.DTSTART, dueDate - estimatedTime) values.put(CalendarContract.Events.DTEND, dueDate) } // setting a duetime to a previously timeless event requires explicitly setting allDay=0 values.put(CalendarContract.Events.ALL_DAY, "0") values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id) } else { values.put(CalendarContract.Events.DTSTART, tzCorrectedDueDate) values.put(CalendarContract.Events.DTEND, tzCorrectedDueDate) values.put(CalendarContract.Events.ALL_DAY, "1") } } else { values.put(CalendarContract.Events.DTSTART, tzCorrectedDueDateNow) values.put(CalendarContract.Events.DTEND, tzCorrectedDueDateNow) values.put(CalendarContract.Events.ALL_DAY, "1") } if ("1" == values[CalendarContract.Events.ALL_DAY]) { values.put(CalendarContract.Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC) } else { values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id) } } companion object { /** If task has no estimated time, how early to set a task in calendar (seconds) */ private const val DEFAULT_CAL_TIME = DateUtilities.ONE_HOUR } }