mirror of https://github.com/tasks/tasks
Sync with opentasks-provider
parent
1ba66f4006
commit
14f46f0688
@ -0,0 +1,204 @@
|
||||
package org.tasks.data
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import at.bitfire.ical4android.UnknownProperty
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.fortuna.ical4j.model.property.XProperty
|
||||
import org.dmfs.tasks.contract.TaskContract.*
|
||||
import org.dmfs.tasks.contract.TaskContract.Property.Category
|
||||
import org.dmfs.tasks.contract.TaskContract.Property.Relation
|
||||
import org.json.JSONObject
|
||||
import org.tasks.R
|
||||
import org.tasks.caldav.iCalendar.Companion.APPLE_SORT_ORDER
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpenTaskDao @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val cr = context.contentResolver
|
||||
val authority = context.getString(R.string.opentasks_authority)
|
||||
|
||||
suspend fun accounts(): List<String> = getLists().map { it.account!! }.distinct()
|
||||
|
||||
@Deprecated("add davx5/etesync accounts manually")
|
||||
suspend fun accountCount(): Int = accounts().size
|
||||
|
||||
suspend fun getLists(): List<CaldavCalendar> = withContext(Dispatchers.IO) {
|
||||
val calendars = ArrayList<CaldavCalendar>()
|
||||
cr.query(
|
||||
TaskLists.getContentUri(authority),
|
||||
null,
|
||||
"${TaskListColumns.SYNC_ENABLED}=1 AND ($ACCOUNT_TYPE = '$ACCOUNT_TYPE_DAVx5' OR $ACCOUNT_TYPE = '$ACCOUNT_TYPE_ETESYNC')",
|
||||
null,
|
||||
null)?.use {
|
||||
while (it.moveToNext()) {
|
||||
val accountType = it.getString(TaskLists.ACCOUNT_TYPE)
|
||||
val accountName = it.getString(TaskLists.ACCOUNT_NAME)
|
||||
calendars.add(CaldavCalendar().apply {
|
||||
id = it.getLong(TaskLists._ID)
|
||||
account = "$accountType:$accountName"
|
||||
name = it.getString(TaskLists.LIST_NAME)
|
||||
color = it.getInt(TaskLists.LIST_COLOR)
|
||||
url = it.getString(CommonSyncColumns._SYNC_ID)
|
||||
ctag = it.getString(TaskLists.SYNC_VERSION)
|
||||
?.let(::JSONObject)
|
||||
?.getString("value")
|
||||
})
|
||||
}
|
||||
}
|
||||
calendars
|
||||
}
|
||||
|
||||
suspend fun getEtags(listId: Long): List<Pair<String, String>> = withContext(Dispatchers.IO) {
|
||||
val items = ArrayList<Pair<String, String>>()
|
||||
cr.query(
|
||||
Tasks.getContentUri(authority),
|
||||
arrayOf(Tasks._SYNC_ID, "version"),
|
||||
"${Tasks.LIST_ID} = $listId",
|
||||
null,
|
||||
null)?.use {
|
||||
while (it.moveToNext()) {
|
||||
items.add(Pair(it.getString(Tasks._SYNC_ID)!!, it.getLong("version").toString()))
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
suspend fun delete(listId: Long, item: String): Int = withContext(Dispatchers.IO) {
|
||||
cr.delete(
|
||||
Tasks.getContentUri(authority),
|
||||
"${Tasks.LIST_ID} = $listId AND ${Tasks._SYNC_ID} = '$item'",
|
||||
null)
|
||||
}
|
||||
|
||||
suspend fun getId(uid: String?): Long =
|
||||
uid?.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
cr.query(
|
||||
Tasks.getContentUri(authority),
|
||||
arrayOf(Tasks._ID),
|
||||
"${Tasks._UID} = '$uid'",
|
||||
null,
|
||||
null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getLong(Tasks._ID)
|
||||
} else {
|
||||
Timber.e("No task with uid=$uid")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: 0L
|
||||
|
||||
suspend fun getTags(caldavTask: CaldavTask): List<String> = withContext(Dispatchers.IO) {
|
||||
val id = getId(caldavTask.remoteId)
|
||||
val tags = ArrayList<String>()
|
||||
cr.query(
|
||||
Properties.getContentUri(authority),
|
||||
arrayOf(Properties.DATA1),
|
||||
"${Properties.TASK_ID} = $id AND ${Properties.MIMETYPE} = '${Category.CONTENT_ITEM_TYPE}'",
|
||||
null,
|
||||
null)?.use {
|
||||
while (it.moveToNext()) {
|
||||
it.getString(Properties.DATA1)?.let(tags::add)
|
||||
}
|
||||
}
|
||||
return@withContext tags
|
||||
}
|
||||
|
||||
suspend fun setTags(caldavTask: CaldavTask, tags: List<String>) = withContext(Dispatchers.IO) {
|
||||
val id = getId(caldavTask.remoteId)
|
||||
cr.delete(
|
||||
Properties.getContentUri(authority),
|
||||
"${Properties.TASK_ID} = $id AND ${Properties.MIMETYPE} = '${Category.CONTENT_ITEM_TYPE}'",
|
||||
null)
|
||||
tags.forEach {
|
||||
cr.insert(Properties.getContentUri(authority), ContentValues().apply {
|
||||
put(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE)
|
||||
put(Category.TASK_ID, id)
|
||||
put(Category.CATEGORY_NAME, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getRemoteOrder(caldavTask: CaldavTask): Long? = withContext(Dispatchers.IO) {
|
||||
val id = getId(caldavTask.remoteId)
|
||||
cr.query(
|
||||
Properties.getContentUri(authority),
|
||||
arrayOf(Properties.DATA0),
|
||||
"${Properties.TASK_ID} = $id AND ${Properties.MIMETYPE} = '${UnknownProperty.CONTENT_ITEM_TYPE}' AND ${Properties.DATA0} LIKE '%$APPLE_SORT_ORDER%'",
|
||||
null,
|
||||
null)?.use {
|
||||
while (it.moveToNext()) {
|
||||
it.getString(Properties.DATA0)
|
||||
?.let(UnknownProperty::fromJsonString)
|
||||
?.takeIf { xprop -> xprop.name.equals(APPLE_SORT_ORDER, true) }
|
||||
?.let { xprop ->
|
||||
return@withContext xprop.value.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
suspend fun setRemoteOrder(caldavTask: CaldavTask) = withContext(Dispatchers.IO) {
|
||||
val id = getId(caldavTask.remoteId)
|
||||
cr.delete(
|
||||
Properties.getContentUri(authority),
|
||||
"${Properties.TASK_ID} = $id AND ${Properties.MIMETYPE} = '${UnknownProperty.CONTENT_ITEM_TYPE}' AND ${Properties.DATA0} LIKE '%$APPLE_SORT_ORDER%'",
|
||||
null)
|
||||
caldavTask.order?.let {
|
||||
cr.insert(Properties.getContentUri(authority), ContentValues().apply {
|
||||
put(Properties.MIMETYPE, UnknownProperty.CONTENT_ITEM_TYPE)
|
||||
put(Properties.TASK_ID, id)
|
||||
put(Properties.DATA0, UnknownProperty.toJsonString(XProperty(APPLE_SORT_ORDER, it.toString())))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateParent(caldavTask: CaldavTask) = withContext(Dispatchers.IO) {
|
||||
caldavTask.remoteParent
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
cr.insert(Properties.getContentUri(authority), ContentValues().apply {
|
||||
put(Relation.MIMETYPE, Relation.CONTENT_ITEM_TYPE)
|
||||
put(Relation.TASK_ID, getId(caldavTask.remoteId))
|
||||
put(Relation.RELATED_TYPE, Relation.RELTYPE_PARENT)
|
||||
put(Relation.RELATED_ID, getId(caldavTask.remoteParent))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getParent(id: Long): String? = withContext(Dispatchers.IO) {
|
||||
cr.query(
|
||||
Properties.getContentUri(authority),
|
||||
arrayOf(Relation.RELATED_UID),
|
||||
"${Relation.TASK_ID} = $id AND ${Properties.MIMETYPE} = '${Relation.CONTENT_ITEM_TYPE}' AND ${Relation.RELATED_TYPE} = ${Relation.RELTYPE_PARENT}",
|
||||
null,
|
||||
null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
it.getString(Relation.RELATED_UID)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACCOUNT_TYPE_DAVx5 = "bitfire.at.davdroid"
|
||||
const val ACCOUNT_TYPE_ETESYNC = "com.etesync.syncadapter"
|
||||
|
||||
fun Cursor.getString(columnName: String): String? =
|
||||
getString(getColumnIndex(columnName))
|
||||
|
||||
fun Cursor.getInt(columnName: String): Int =
|
||||
getInt(getColumnIndex(columnName))
|
||||
|
||||
fun Cursor.getLong(columnName: String): Long =
|
||||
getLong(getColumnIndex(columnName))
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.Assisted
|
||||
import androidx.hilt.work.WorkerInject
|
||||
import androidx.work.WorkerParameters
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
|
||||
import org.tasks.data.CaldavDao
|
||||
import org.tasks.data.OpenTaskDao
|
||||
import org.tasks.opentasks.OpenTasksSynchronizer
|
||||
import org.tasks.preferences.Preferences
|
||||
|
||||
class SyncOpenTasksWork @WorkerInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
firebase: Firebase,
|
||||
localBroadcastManager: LocalBroadcastManager,
|
||||
preferences: Preferences,
|
||||
private val openTasksSynchronizer: OpenTasksSynchronizer,
|
||||
private val caldavDao: CaldavDao,
|
||||
private val openTaskDao: OpenTaskDao
|
||||
) : SyncWork(context, workerParams, firebase, localBroadcastManager, preferences) {
|
||||
override val syncStatus = R.string.p_sync_ongoing_opentasks
|
||||
|
||||
override suspend fun enabled() =
|
||||
caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty()
|
||||
|| openTaskDao.accountCount() > 0
|
||||
|
||||
override suspend fun doSync() {
|
||||
openTasksSynchronizer.sync()
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package org.tasks.opentasks
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OpenTaskAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding!!.userLayout.visibility = View.GONE
|
||||
binding!!.passwordLayout.visibility = View.GONE
|
||||
binding!!.urlLayout.visibility = View.GONE
|
||||
}
|
||||
|
||||
override val description: Int
|
||||
get() = 0
|
||||
|
||||
override val newPassword: String?
|
||||
get() = ""
|
||||
|
||||
private suspend fun updateAccount(principal: String?) {
|
||||
hideProgressIndicator()
|
||||
caldavAccount!!.name = newName
|
||||
caldavAccount!!.url = principal
|
||||
caldavAccount!!.username = newUsername
|
||||
caldavAccount!!.error = ""
|
||||
if (passwordChanged()) {
|
||||
caldavAccount!!.password = encryption.encrypt(newPassword!!)
|
||||
}
|
||||
caldavAccount!!.isSuppressRepeatingTasks = binding!!.repeat.isChecked
|
||||
caldavDao.update(caldavAccount!!)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun hasChanges() =
|
||||
newName != caldavAccount!!.name
|
||||
|| binding!!.repeat.isChecked != caldavAccount!!.isSuppressRepeatingTasks
|
||||
|
||||
override fun save() = lifecycleScope.launch {
|
||||
if (newName.isBlank()) {
|
||||
binding!!.nameLayout.error = getString(R.string.name_cannot_be_empty)
|
||||
return@launch
|
||||
}
|
||||
updateAccount()
|
||||
}
|
||||
|
||||
override suspend fun addAccount(url: String, username: String, password: String) {}
|
||||
|
||||
override suspend fun updateAccount(url: String, username: String, password: String) {}
|
||||
|
||||
override suspend fun updateAccount() = updateAccount(caldavAccount!!.url)
|
||||
|
||||
override val helpUrl: String
|
||||
get() = "https://tasks.org/sync"
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.tasks.opentasks
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import org.dmfs.tasks.contract.TaskContract.*
|
||||
import org.tasks.R
|
||||
import org.tasks.sync.SyncAdapters
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class OpenTaskContentObserver @Inject constructor(
|
||||
private val syncAdapters: SyncAdapters
|
||||
) : ContentObserver(getHandler()) {
|
||||
|
||||
override fun onChange(selfChange: Boolean) = onChange(selfChange, null)
|
||||
|
||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||
if (selfChange || uri == null) {
|
||||
Timber.d("Ignoring onChange(selfChange = $selfChange, uri = $uri)")
|
||||
return
|
||||
} else {
|
||||
Timber.v("onChange($selfChange, $uri)")
|
||||
}
|
||||
|
||||
syncAdapters.syncOpenTasks()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getHandler() = HandlerThread("OT-handler)").let {
|
||||
it.start()
|
||||
Handler(it.looper)
|
||||
}
|
||||
|
||||
fun registerObserver(context: Context, observer: ContentObserver) {
|
||||
getUris(context.getString(R.string.opentasks_authority))
|
||||
.forEach {
|
||||
context.contentResolver.registerContentObserver(it, false, observer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUris(authority: String): List<Uri> =
|
||||
listOf(TaskLists.getContentUri(authority),
|
||||
Tasks.getContentUri(authority),
|
||||
Properties.getContentUri(authority))
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.tasks.opentasks
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import butterknife.BindView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.tasks.R
|
||||
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
|
||||
import org.tasks.data.CaldavAccount
|
||||
import org.tasks.data.CaldavCalendar
|
||||
|
||||
@AndroidEntryPoint
|
||||
class OpenTasksListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
|
||||
|
||||
@BindView(R.id.color_row)
|
||||
lateinit var colorRow: RelativeLayout
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
toolbar.menu.findItem(R.id.delete).isVisible = false
|
||||
nameLayout.visibility = View.GONE
|
||||
colorRow.visibility = View.GONE
|
||||
}
|
||||
|
||||
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) {}
|
||||
|
||||
override suspend fun updateNameAndColor(
|
||||
account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) =
|
||||
updateCalendar()
|
||||
|
||||
override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) {}
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
package org.tasks.opentasks
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import com.todoroo.andlib.utility.DateUtilities
|
||||
import com.todoroo.astrid.dao.TaskDao
|
||||
import com.todoroo.astrid.data.Task
|
||||
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY
|
||||
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME
|
||||
import com.todoroo.astrid.data.Task.Companion.sanitizeRRule
|
||||
import com.todoroo.astrid.helper.UUIDHelper
|
||||
import com.todoroo.astrid.service.TaskCreator
|
||||
import com.todoroo.astrid.service.TaskDeleter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.fortuna.ical4j.model.Date
|
||||
import net.fortuna.ical4j.model.property.Geo
|
||||
import net.fortuna.ical4j.model.property.RRule
|
||||
import org.dmfs.tasks.contract.TaskContract.Tasks
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.caldav.CaldavConverter
|
||||
import org.tasks.caldav.CaldavConverter.toRemote
|
||||
import org.tasks.caldav.iCalendar
|
||||
import org.tasks.data.*
|
||||
import org.tasks.data.OpenTaskDao.Companion.getInt
|
||||
import org.tasks.data.OpenTaskDao.Companion.getLong
|
||||
import org.tasks.data.OpenTaskDao.Companion.getString
|
||||
import org.tasks.date.DateTimeUtils.newDateTime
|
||||
import org.tasks.time.DateTime
|
||||
import org.tasks.time.DateTimeUtils.currentTimeMillis
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class OpenTasksSynchronizer @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val caldavDao: CaldavDao,
|
||||
private val taskDeleter: TaskDeleter,
|
||||
private val localBroadcastManager: LocalBroadcastManager,
|
||||
private val taskCreator: TaskCreator,
|
||||
private val taskDao: TaskDao,
|
||||
private val firebase: Firebase,
|
||||
private val iCalendar: iCalendar,
|
||||
private val locationDao: LocationDao,
|
||||
private val openTaskDao: OpenTaskDao,
|
||||
private val tagDao: TagDao,
|
||||
private val tagDataDao: TagDataDao,
|
||||
private val inventory: Inventory) {
|
||||
|
||||
private val cr = context.contentResolver
|
||||
|
||||
suspend fun sync() {
|
||||
val lists = getLists()
|
||||
caldavDao.getAccounts(CaldavAccount.TYPE_OPENTASKS).forEach { account ->
|
||||
if (!lists.containsKey(account.uuid)) {
|
||||
setError(account, context.getString(R.string.account_not_found))
|
||||
} else if (!inventory.hasPro()) {
|
||||
setError(account, context.getString(R.string.requires_pro_subscription))
|
||||
} else {
|
||||
sync(account, lists[account.uuid]!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLists(): Map<String, List<CaldavCalendar>> =
|
||||
openTaskDao.getLists().groupBy { it.account!! }
|
||||
|
||||
private suspend fun sync(account: CaldavAccount, lists: List<CaldavCalendar>) {
|
||||
caldavDao
|
||||
.findDeletedCalendars(account.uuid!!, lists.mapNotNull { it.url })
|
||||
.forEach { taskDeleter.delete(it) }
|
||||
lists.forEach {
|
||||
val calendar = toLocalCalendar(account.uuid!!, it)
|
||||
sync(calendar, it.ctag, it.id)
|
||||
}
|
||||
setError(account, null)
|
||||
}
|
||||
|
||||
private suspend fun toLocalCalendar(account: String, remote: CaldavCalendar): CaldavCalendar {
|
||||
val local = caldavDao.getCalendarByUrl(account, remote.url!!) ?: CaldavCalendar().apply {
|
||||
uuid = UUIDHelper.newUUID()
|
||||
url = remote.url
|
||||
this.account = account
|
||||
caldavDao.insert(this)
|
||||
Timber.d("Created calendar: $this")
|
||||
}
|
||||
if (local.name != remote.name || local.color != remote.color) {
|
||||
local.color = remote.color
|
||||
local.name = remote.name
|
||||
caldavDao.update(local)
|
||||
Timber.d("Updated calendar: $local")
|
||||
localBroadcastManager.broadcastRefreshList()
|
||||
}
|
||||
return local
|
||||
}
|
||||
|
||||
private suspend fun sync(calendar: CaldavCalendar, ctag: String?, listId: Long) {
|
||||
Timber.d("SYNC $calendar")
|
||||
|
||||
caldavDao.getDeleted(calendar.uuid!!).forEach {
|
||||
openTaskDao.delete(listId, it.`object`!!)
|
||||
caldavDao.delete(it)
|
||||
}
|
||||
|
||||
taskDao
|
||||
.getCaldavTasksToPush(calendar.uuid!!)
|
||||
.mapNotNull { push(it, listId) }
|
||||
.forEach {
|
||||
val tags = tagDataDao.getTagDataForTask(it.task).mapNotNull(TagData::name)
|
||||
openTaskDao.setTags(it, tags)
|
||||
openTaskDao.setRemoteOrder(it)
|
||||
openTaskDao.updateParent(it)
|
||||
it.lastSync = currentTimeMillis()
|
||||
caldavDao.update(it)
|
||||
Timber.d("SENT $it")
|
||||
}
|
||||
|
||||
ctag?.let {
|
||||
if (ctag == calendar.ctag) {
|
||||
Timber.d("UP TO DATE: $calendar")
|
||||
return@sync
|
||||
}
|
||||
}
|
||||
|
||||
val etags = openTaskDao.getEtags(listId)
|
||||
etags.forEach { (syncId, etag) ->
|
||||
val caldavTask = caldavDao.getTask(calendar.uuid!!, syncId)
|
||||
applyChanges(calendar, listId, syncId, etag, caldavTask)
|
||||
}
|
||||
removeDeleted(calendar.uuid!!, etags.map { it.first })
|
||||
|
||||
calendar.ctag = ctag
|
||||
Timber.d("UPDATE $calendar")
|
||||
caldavDao.update(calendar)
|
||||
caldavDao.updateParents(calendar.uuid!!)
|
||||
localBroadcastManager.broadcastRefresh()
|
||||
}
|
||||
|
||||
private suspend fun removeDeleted(calendar: String, objects: List<String>) {
|
||||
caldavDao
|
||||
.getObjects(calendar)
|
||||
.subtract(objects)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let {
|
||||
Timber.d("DELETED $it")
|
||||
taskDeleter.delete(caldavDao.getTasks(calendar, it.toList()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setError(account: CaldavAccount, message: String?) {
|
||||
account.error = message
|
||||
caldavDao.update(account)
|
||||
localBroadcastManager.broadcastRefreshList()
|
||||
if (!message.isNullOrBlank()) {
|
||||
Timber.e(message)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun push(task: Task, listId: Long): CaldavTask? = withContext(Dispatchers.IO) {
|
||||
val caldavTask = caldavDao.getTask(task.id) ?: return@withContext null
|
||||
if (task.isDeleted) {
|
||||
openTaskDao.delete(listId, caldavTask.`object`!!)
|
||||
taskDeleter.delete(task)
|
||||
return@withContext null
|
||||
}
|
||||
val values = ContentValues()
|
||||
values.put(Tasks._SYNC_ID, caldavTask.`object`)
|
||||
values.put(Tasks.LIST_ID, listId)
|
||||
values.put(Tasks.TITLE, task.title)
|
||||
values.put(Tasks.DESCRIPTION, task.notes)
|
||||
values.put(Tasks.GEO, locationDao.getGeofences(task.id).toGeoString())
|
||||
values.put(Tasks.RRULE, if (task.isRecurring) {
|
||||
val rrule = RRule(task.getRecurrenceWithoutFrom()!!.replace("RRULE:", ""))
|
||||
if (task.repeatUntil > 0) {
|
||||
rrule.recur.until = DateTime(task.repeatUntil).toUTC().toDateTime()
|
||||
}
|
||||
RRule(rrule.value.sanitizeRRule()).value
|
||||
} else {
|
||||
null
|
||||
})
|
||||
values.put(Tasks.IS_ALLDAY, if (task.hasDueDate() && !task.hasDueTime()) 1 else 0)
|
||||
values.put(Tasks.DUE, when {
|
||||
task.hasDueTime() -> newDateTime(task.dueDate).toDateTime().time
|
||||
task.hasDueDate() -> Date(task.dueDate).time
|
||||
else -> null
|
||||
})
|
||||
values.put(Tasks.COMPLETED_IS_ALLDAY, 0)
|
||||
values.put(Tasks.COMPLETED, if (task.isCompleted) task.completionDate else null)
|
||||
values.put(Tasks.STATUS, if (task.isCompleted) Tasks.STATUS_COMPLETED else null)
|
||||
values.put(Tasks.PERCENT_COMPLETE, if (task.isCompleted) 100 else null)
|
||||
values.put(Tasks.TZ, if (task.hasDueTime() || task.isCompleted) {
|
||||
TimeZone.getDefault().id
|
||||
} else {
|
||||
null
|
||||
})
|
||||
values.put(Tasks.PARENT_ID, null as Long?)
|
||||
val existing = cr.query(
|
||||
Tasks.getContentUri(openTaskDao.authority),
|
||||
arrayOf(Tasks.PRIORITY),
|
||||
"${Tasks.LIST_ID} = $listId AND ${Tasks._SYNC_ID} = '${caldavTask.`object`}'",
|
||||
null,
|
||||
null)?.use {
|
||||
if (!it.moveToFirst()) {
|
||||
return@use false
|
||||
}
|
||||
values.put(Tasks.PRIORITY, toRemote(it.getInt(Tasks.PRIORITY), task.priority))
|
||||
true
|
||||
} ?: false
|
||||
try {
|
||||
if (existing) {
|
||||
val updated = cr.update(
|
||||
Tasks.getContentUri(openTaskDao.authority),
|
||||
values,
|
||||
"${Tasks.LIST_ID} = $listId AND ${Tasks._SYNC_ID} = '${caldavTask.`object`}'",
|
||||
null)
|
||||
if (updated <= 0) {
|
||||
throw Exception("update failed")
|
||||
}
|
||||
} else {
|
||||
values.put(Tasks._UID, caldavTask.remoteId)
|
||||
values.put(Tasks.PRIORITY, toRemote(task.priority, task.priority))
|
||||
cr.insert(Tasks.getContentUri(openTaskDao.authority), values)
|
||||
?: throw Exception("insert returned null")
|
||||
}
|
||||
caldavTask
|
||||
} catch (e: Exception) {
|
||||
firebase.reportException(e)
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun applyChanges(
|
||||
calendar: CaldavCalendar,
|
||||
listId: Long,
|
||||
syncId: String,
|
||||
etag: String,
|
||||
existing: CaldavTask?) {
|
||||
cr.query(
|
||||
Tasks.getContentUri(openTaskDao.authority),
|
||||
null,
|
||||
"${Tasks.LIST_ID} = $listId AND ${Tasks._SYNC_ID} = '$syncId'",
|
||||
null,
|
||||
null)?.use {
|
||||
if (!it.moveToFirst()) {
|
||||
return
|
||||
}
|
||||
val task: Task
|
||||
val caldavTask: CaldavTask
|
||||
if (existing == null) {
|
||||
task = taskCreator.createWithValues("")
|
||||
taskDao.createNew(task)
|
||||
val remoteId = it.getString(Tasks._UID)
|
||||
caldavTask = CaldavTask(task.id, calendar.uuid, remoteId, syncId)
|
||||
} else {
|
||||
task = taskDao.fetch(existing.task)!!
|
||||
caldavTask = existing
|
||||
}
|
||||
if (caldavTask.etag == null || caldavTask.etag != etag) {
|
||||
task.title = it.getString(Tasks.TITLE)
|
||||
task.priority = CaldavConverter.fromRemote(it.getInt(Tasks.PRIORITY))
|
||||
task.completionDate = it.getLong(Tasks.COMPLETED)
|
||||
task.notes = it.getString(Tasks.DESCRIPTION)
|
||||
task.modificationDate = currentTimeMillis()
|
||||
task.creationDate = it.getLong(Tasks.CREATED).toLocal()
|
||||
task.setDueDateAdjustingHideUntil(it.getLong(Tasks.DUE).let { due ->
|
||||
when {
|
||||
due == 0L -> 0
|
||||
it.getBoolean(Tasks.IS_ALLDAY) ->
|
||||
Task.createDueDate(URGENCY_SPECIFIC_DAY, due - DateTime(due).offset)
|
||||
else -> Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, due)
|
||||
}
|
||||
})
|
||||
iCalendar.setPlace(task.id, it.getString(Tasks.GEO).toGeo())
|
||||
task.setRecurrence(it.getString(Tasks.RRULE).toRRule())
|
||||
task.suppressSync()
|
||||
task.suppressRefresh()
|
||||
taskDao.save(task)
|
||||
caldavTask.lastSync = DateUtilities.now() + 1000L
|
||||
caldavTask.etag = etag
|
||||
}
|
||||
val tags = openTaskDao.getTags(caldavTask)
|
||||
tagDao.applyTags(task, tagDataDao, iCalendar.getTags(tags))
|
||||
caldavTask.order = openTaskDao.getRemoteOrder(caldavTask)
|
||||
caldavTask.remoteParent = openTaskDao.getParent(it.getLong(Tasks._ID))
|
||||
if (caldavTask.id == Task.NO_ID) {
|
||||
caldavTask.id = caldavDao.insert(caldavTask)
|
||||
Timber.d("NEW $caldavTask")
|
||||
} else {
|
||||
caldavDao.update(caldavTask)
|
||||
Timber.d("UPDATE $caldavTask")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun Location?.toGeoString(): String? = this?.let { "$longitude,$latitude" }
|
||||
|
||||
private fun String?.toGeo(): Geo? =
|
||||
this
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.split(",")
|
||||
?.takeIf {
|
||||
it.size == 2
|
||||
&& it[0].toDoubleOrNull() != null
|
||||
&& it[1].toDoubleOrNull() != null }
|
||||
?.let { Geo("${it[1]};${it[0]}") }
|
||||
|
||||
private fun String?.toRRule(): RRule? =
|
||||
this?.takeIf { it.isNotBlank() }?.let(::RRule)
|
||||
|
||||
private fun Cursor.getBoolean(columnName: String): Boolean =
|
||||
getInt(getColumnIndex(columnName)) != 0
|
||||
|
||||
private fun Long.toLocal(): Long =
|
||||
DateTime(this).toLocal().millis
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
<vector android:height="24dp" android:viewportHeight="1024"
|
||||
android:viewportWidth="1024" android:width="24dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#77A646" android:pathData="M887.67,957.05L140.08,957.05c-39.35,0 -71.25,-31.9 -71.25,-71.25L68.82,138.2c0,-39.35 31.9,-71.25 71.25,-71.25l747.6,0c39.35,0 71.25,31.9 71.25,71.25l0,747.6c0,39.35 -31.9,71.25 -71.25,71.25z"/>
|
||||
<path android:fillColor="#A2CF6E" android:pathData="M887.67,918.68L140.08,918.68c-39.35,0 -71.25,-31.9 -71.25,-71.25L68.82,138.2c0,-39.35 31.9,-71.25 71.25,-71.25l747.6,0c39.35,0 71.25,31.9 71.25,71.25l0,709.23c0,39.35 -31.9,71.25 -71.25,71.25z"/>
|
||||
<path android:fillColor="#8BC34A" android:pathData="M887.67,935.13L140.08,935.13c-39.35,0 -71.25,-31.9 -71.25,-71.25L68.82,160.12c0,-39.35 31.9,-71.25 71.25,-71.25l747.6,0c39.35,0 71.25,31.9 71.25,71.25l0,703.75c0,39.35 -31.9,71.25 -71.25,71.25z"/>
|
||||
<group>
|
||||
<clip-path android:pathData="M887.67,935.13L140.08,935.13c-39.35,0 -71.25,-31.9 -71.25,-71.25L68.82,160.12c0,-39.35 31.9,-71.25 71.25,-71.25l747.6,0c39.35,0 71.25,31.9 71.25,71.25l0,703.75c0,39.35 -31.9,71.25 -71.25,71.25z"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m201.18,550.35h27.7l707.11,707.11h-27.7z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m228.88,550.35c25.78,0 44.44,-13.44 44.44,-44.99l707.11,707.11c0,31.54 -18.65,44.99 -44.44,44.99z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m273.32,505.36c0,-14.75 -4.08,-25.36 -11.2,-32.48l707.11,707.11c7.12,7.12 11.2,17.73 11.2,32.48z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m262.12,472.88c-8.11,-8.11 -20.15,-11.68 -34.61,-11.68l707.11,707.11c14.46,0 26.51,3.58 34.61,11.68z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m227.51,461.2h-26.33l707.11,707.11h26.33z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="M224.77,531.42L224.77,480.13L931.88,1187.23v51.29z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m224.77,480.13h1.37l707.11,707.11h-1.37z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m226.14,480.13c6.78,0 12.8,1.21 16.99,5.4l707.11,707.11c-4.19,-4.19 -10.21,-5.4 -16.99,-5.4z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m243.13,485.53c3.77,3.77 6.05,9.95 6.05,19.84l707.11,707.11c0,-9.88 -2.28,-16.06 -6.05,-19.84z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m249.18,505.36c0,20.85 -10.15,26.06 -23.04,26.06l707.11,707.11c12.89,0 23.04,-5.21 23.04,-26.06z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m201.18,550.35h27.7c25.78,0 44.44,-13.44 44.44,-44.99 0,-31.54 -18.65,-44.16 -45.81,-44.16h-26.33zM224.77,531.42v-51.29h1.37c12.89,0 23.04,4.39 23.04,25.24 0,20.85 -10.15,26.06 -23.04,26.06z"
|
||||
android:strokeAlpha="0.2" android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m282.62,280.51 l-85.53,-85.53 707.11,707.11 85.53,85.53z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m197.09,194.98 l-0,228.08 707.11,707.11 0,-228.08z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="M197.09,423.06L425.17,423.06L1132.28,1130.17L904.2,1130.17Z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m425.17,423.06 l-85.53,-85.53 707.11,707.11 85.53,85.53z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m339.64,337.53c94.37,-94.37 247.76,-94.37 342.12,0L1388.88,1044.63c-94.37,-94.37 -247.76,-94.37 -342.12,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m681.77,337.53c1.95,1.95 3.86,3.85 5.73,5.72l707.11,707.11c-1.87,-1.87 -3.78,-3.77 -5.73,-5.72z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m687.49,343.25c25.77,25.77 44.04,44.85 54.15,78.88l707.11,707.11c-10.1,-34.02 -28.38,-53.11 -54.15,-78.88z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m741.64,422.13h83.25l707.11,707.11h-83.25z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="M824.89,422.13C812.06,364.82 783.55,325.27 738.79,280.51l707.11,707.11c44.76,44.76 73.27,84.31 86.1,141.62z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m738.79,280.51c-0,-0 -0,-0 -0,-0l707.11,707.11c0,0 0,0 0,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m738.79,280.51c0,0 -0,-0 -0,-0l707.11,707.11c0,0 0,0 0,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m738.79,280.51c-126.02,-126.02 -330.15,-126.02 -456.17,0l707.11,707.11c126.02,-126.02 330.15,-126.02 456.17,-0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m681.77,679.65c-94.37,94.37 -247.76,94.37 -342.12,0l707.11,707.11c94.37,94.37 247.76,94.37 342.12,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m339.64,679.65c-0,-0 -0,-0 -0,-0l707.11,707.11c0,0 0,0 0,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m339.64,679.65c-1.95,-1.95 -3.86,-3.85 -5.73,-5.72l707.11,707.11c1.87,1.87 3.78,3.78 5.73,5.72z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m333.91,673.93c-25.76,-25.76 -44.04,-44.85 -54.14,-78.88l707.11,707.11c10.1,34.02 28.38,53.11 54.14,78.88z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m279.77,595.05h-83.25l707.11,707.11h83.25z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m196.52,595.05c12.83,57.31 41.34,96.86 86.1,141.62l707.11,707.11c-44.76,-44.76 -73.27,-84.32 -86.1,-141.62z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m282.62,736.67c0,0 0,0 0,0L989.73,1443.78c-0,0 -0,-0 -0,-0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m282.62,736.67c0,0 0,0 0,0L989.73,1443.78c-0,0 -0,0 -0,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m282.62,736.67c126.02,126.02 330.15,126.02 456.17,-0L1445.9,1443.78c-126.02,126.02 -330.15,126.02 -456.17,0z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m738.79,736.67 l85.53,85.53 707.11,707.11 -85.53,-85.53z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m824.32,822.2 l0,-228.08 707.11,707.11v228.08z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="M824.32,594.12L596.24,594.12l707.11,707.11h228.08z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:pathData="m282.62,280.51 l-85.53,-85.53 -0,228.08h228.08l-85.53,-85.53c94.37,-94.37 247.76,-94.37 342.12,0 28.8,28.8 49.04,48.11 59.87,84.6h83.25c-12.83,-57.31 -41.34,-96.86 -86.1,-141.62 -126.02,-126.02 -330.15,-126.02 -456.17,0zM681.77,679.65c-94.37,94.37 -247.76,94.37 -342.12,0 -28.8,-28.8 -49.04,-48.11 -59.87,-84.6h-83.25c12.83,57.31 41.34,96.86 86.1,141.62 126.02,126.02 330.15,126.02 456.17,0l85.53,85.53 0,-228.08L596.24,594.12Z"
|
||||
android:strokeAlpha="0.2" android:strokeWidth="40.319767"/>
|
||||
</group>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m282.78,280.51 l-85.53,-85.53 -0,228.08h228.08l-85.53,-85.53c94.37,-94.37 247.76,-94.37 342.12,0 28.8,28.8 49.04,48.11 59.87,84.6h83.25c-12.83,-57.31 -41.34,-96.86 -86.1,-141.62 -126.02,-126.02 -330.15,-126.02 -456.17,0zM681.93,679.65c-94.37,94.37 -247.76,94.37 -342.12,0 -28.8,-28.8 -49.04,-48.11 -59.87,-84.6h-83.25c12.83,57.31 41.34,96.86 86.1,141.62 126.02,126.02 330.15,126.02 456.17,0l85.53,85.53 0,-228.08L596.4,594.12Z" android:strokeWidth="40.319767"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m201.34,550.35h27.7c25.78,0 44.44,-13.44 44.44,-44.99 0,-31.54 -18.65,-44.16 -45.81,-44.16h-26.33zM224.93,531.42v-51.29h1.37c12.89,0 23.04,4.39 23.04,25.24 0,20.85 -10.15,26.06 -23.04,26.06z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m311.14,507.01c2.19,-8.5 4.39,-19.2 6.31,-28.25h0.55c2.19,8.91 4.39,19.75 6.58,28.25l1.51,6.17L309.63,513.18ZM276.85,550.35h24.14l4.39,-18.93h24.96l4.39,18.93h24.96l-27.16,-89.15h-28.53z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m380.57,550.35h28.53l26.33,-89.15h-24.14l-9.05,38.95c-2.33,9.46 -4.11,18.65 -6.58,28.25h-0.55c-2.47,-9.6 -4.11,-18.79 -6.58,-28.25l-9.33,-38.95h-24.96z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="3.42874193"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m449.65,601.24h39.36l6.19,-15.48c2.43,-6.41 5.09,-12.82 7.52,-19.01h0.88c3.32,6.19 6.41,12.82 9.73,19.01l8.84,15.48h40.68l-33.17,-53.06 31.4,-57.49h-39.36l-5.31,15.48c-1.99,6.19 -4.64,12.82 -6.63,19.01h-0.88c-2.87,-6.19 -5.97,-12.82 -8.84,-19.01l-7.96,-15.48h-40.68l31.4,53.06z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="5.52750778"/>
|
||||
<path android:fillColor="#ffffff"
|
||||
android:pathData="m601.96,514.52c18.54,0 34.49,-11.78 34.49,-32.19 0,-19.26 -13.51,-28.17 -29.32,-28.17 -3.02,0 -5.46,0.29 -8.62,1.44l1.15,-13.22h32.77v-20.69h-54.04l-2.3,46.85 10.63,6.9c5.46,-3.45 7.76,-4.31 12.65,-4.31 7.19,0 12.36,4.02 12.36,11.78 0,8.05 -4.89,11.78 -13.51,11.78 -6.61,0 -12.93,-3.74 -18.4,-8.62l-10.92,15.52c7.47,7.47 18.11,12.93 33.05,12.93z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="5.52750778"/>
|
||||
</vector>
|
@ -1,7 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="changelog">
|
||||
<item>Fix Google Task bugs</item>
|
||||
<item>Join Tasks on Reddit: https://reddit.com/r/tasks</item>
|
||||
<item>🚧 ALPHA VERSION 🚧</item>
|
||||
<item>PRO: DAVx⁵ support (alpha requires custom DAVx⁵ build)</item>
|
||||
<item>PRO: EteSync client support (alpha requires custom EteSync client build)</item>
|
||||
<item>ToDo Agenda integration</item>
|
||||
<item>Changed backstack behavior to follow Android conventions</item>
|
||||
<item>Major internal changes! Please report any bugs!</item>
|
||||
<item>Remove Mapbox tiles (Google Play only)</item>
|
||||
<item>Added \'Astrid manual sort\' information to backup file</item>
|
||||
<item>Bug fixes</item>
|
||||
<item>Performance improvements</item>
|
||||
<item>Security improvements</item>
|
||||
<item>Update translations</item>
|
||||
<item>Find Tasks on Reddit: https://reddit.com/r/tasks</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue