diff --git a/CHANGELOG.md b/CHANGELOG.md
index 301c6e24d..e21ccdb63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,42 @@
Change Log
---
=======
+### 10.0 (2020-08-03)
+
+🚧 Currently in alpha 🚧
+
+* PRO: DAVx⁵ support (alpha requires custom DAVx⁵ build)
+* PRO: EteSync client support (alpha requires custom EteSync client build)
+* [ToDo Agenda](https://play.google.com/store/apps/details?id=org.andstatus.todoagenda) integration
+* Changed backstack behavior to follow Android conventions
+* Major internal changes! Please report any bugs!
+* Remove Mapbox tiles (Google Play only)
+* Added 'Astrid manual sort' information to backup file
+* Bug fixes
+* Performance improvements
+* Security improvements
+* Update translations
+ * Basque - @osoitz
+ * Bengali - @Oymate
+ * Brazilian Portuguese - Pedro Lucas Porcellis
+ * Chinese - WH Julie
+ * Czech - @vitSkalicky, Radek Řehořek
+ * Dutch - @fvbommel
+ * Finnish - J. Lavoie
+ * French - @FlorianLeChat, J. Lavoie, @sephrat
+ * German - @franconian
+ * Hebrew - @yarons, @avipars
+ * Hungarian - kaciokos
+ * Italian - @ppasserini, J. Lavoie
+ * Norwegian Bokmål - @comradekingu, Erlend Ydse
+ * Polish - @alex-ter
+ * Portuguese - @SantosSi
+ * Russian - Nikita Epifanov
+ * Simplified Chinese - @sr093906, @cccClyde
+ * Spanish - @FlorianLeChat
+ * Tamil - @balogic, @Thiya-velu
+ * Turkish - @emintufan
+
### 9.7.3 (2020-07-07)
* Fix Google Task bugs
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 352bb2112..116553107 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -44,8 +44,8 @@ android {
defaultConfig {
testApplicationId = "org.tasks.test"
applicationId = "org.tasks"
- versionCode = 90704
- versionName = "9.7.3"
+ versionCode = 100000
+ versionName = "10.0"
targetSdkVersion(Versions.targetSdk)
minSdkVersion(Versions.minSdk)
testInstrumentationRunner = "org.tasks.TestRunner"
diff --git a/app/licenses.yml b/app/licenses.yml
index 32934fe44..0186a0702 100644
--- a/app/licenses.yml
+++ b/app/licenses.yml
@@ -774,6 +774,7 @@
license: The Apache Software License, Version 2.0
licenseUrl: https://api.github.com/licenses/apache-2.0
url: https://github.com/dmfs/opentasks
+ forceGenerate: true
- artifact: org.dmfs:lib-recur:+
name: lib-recur
copyrightHolder: Marten Gajda
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c7691d3af..8a1087a69 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -344,6 +344,10 @@
android:name=".etesync.EteSyncAccountSettingsActivity"
android:theme="@style/Tasks" />
+
+
@@ -352,6 +356,10 @@
android:name=".caldav.LocalListSettingsActivity"
android:theme="@style/Tasks"/>
+
+
diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt
index 01bc53632..c9e54be16 100644
--- a/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt
+++ b/app/src/main/java/com/todoroo/astrid/service/TaskMover.kt
@@ -54,7 +54,9 @@ class TaskMover @Inject constructor(
if (selectedList is CaldavFilter) {
caldavDao.updateParents(selectedList.uuid)
}
- taskDao.touch(tasks)
+ tasks.dbchunk().forEach {
+ taskDao.touch(it)
+ }
localBroadcastManager.broadcastRefresh()
syncAdapters.sync()
}
diff --git a/app/src/main/java/org/tasks/Tasks.kt b/app/src/main/java/org/tasks/Tasks.kt
index 38363328b..f1ea96169 100644
--- a/app/src/main/java/org/tasks/Tasks.kt
+++ b/app/src/main/java/org/tasks/Tasks.kt
@@ -22,6 +22,7 @@ import org.tasks.injection.InjectingJobIntentService
import org.tasks.jobs.WorkManager
import org.tasks.locale.Locale
import org.tasks.location.GeofenceApi
+import org.tasks.opentasks.OpenTaskContentObserver
import org.tasks.preferences.Preferences
import org.tasks.receivers.RefreshReceiver
import org.tasks.scheduling.CalendarNotificationIntentService
@@ -47,12 +48,14 @@ class Tasks : Application(), Configuration.Provider {
@Inject lateinit var billingClient: Lazy
@Inject lateinit var appWidgetManager: Lazy
@Inject lateinit var workerFactory: HiltWorkerFactory
+ @Inject lateinit var contentObserver: Lazy
override fun onCreate() {
super.onCreate()
buildSetup.setup()
upgrade()
preferences.isSyncOngoing = false
+ preferences.setBoolean(R.string.p_sync_ongoing_opentasks, false)
ThemeBase.getThemeBase(preferences, inventory, null).setDefaultNightMode()
localBroadcastManager.registerRefreshReceiver(RefreshBroadcastReceiver())
Locale.getInstance(this).createConfigurationContext(applicationContext)
@@ -80,6 +83,7 @@ class Tasks : Application(), Configuration.Provider {
scheduleMidnightRefresh()
scheduleBackup()
scheduleConfigRefresh()
+ OpenTaskContentObserver.registerObserver(context, contentObserver.get())
}
geofenceApi.get().registerAll()
FileHelper.delete(context, preferences.cacheDirectory)
diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt
index 2b4e7c95b..66e49ca75 100644
--- a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt
+++ b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt
@@ -171,7 +171,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
protected abstract val newPassword: String?
- private fun save() = lifecycleScope.launch {
+ protected open fun save() = lifecycleScope.launch {
if (requestInProgress()) {
return@launch
}
@@ -282,7 +282,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
return snackbar
}
- private fun hasChanges(): Boolean {
+ protected open fun hasChanges(): Boolean {
return if (caldavAccount == null) {
(!isNullOrEmpty(binding!!.name.text.toString().trim { it <= ' ' })
|| !isNullOrEmpty(newPassword)
diff --git a/app/src/main/java/org/tasks/caldav/CaldavConverter.java b/app/src/main/java/org/tasks/caldav/CaldavConverter.java
index 8795cb629..40b7ccde6 100644
--- a/app/src/main/java/org/tasks/caldav/CaldavConverter.java
+++ b/app/src/main/java/org/tasks/caldav/CaldavConverter.java
@@ -68,7 +68,7 @@ public class CaldavConverter {
}
}
- private static @Priority int fromRemote(int remotePriority) {
+ public static @Priority int fromRemote(int remotePriority) {
// https://tools.ietf.org/html/rfc5545#section-3.8.1.9
if (remotePriority == 0) {
return Priority.NONE;
@@ -79,7 +79,7 @@ public class CaldavConverter {
return remotePriority < 5 ? Priority.HIGH : Priority.LOW;
}
- private static int toRemote(int remotePriority, int localPriority) {
+ public static int toRemote(int remotePriority, int localPriority) {
if (localPriority == Priority.NONE) {
return 0;
}
diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
index 40dc862c3..455bd37e6 100644
--- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
+++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt
@@ -209,14 +209,14 @@ class CaldavSynchronizer @Inject constructor(
iCal.fromVtodo(caldavCalendar, caldavTask, remote, vtodo, fileName, eTag.eTag)
}
}
- val deleted = caldavDao
+ caldavDao
.getObjects(caldavCalendar.uuid!!)
.subtract(members.map { it.hrefName() })
- .toList()
- if (deleted.isNotEmpty()) {
- Timber.d("DELETED %s", deleted)
- taskDeleter.delete(caldavDao.getTasks(caldavCalendar.uuid!!, deleted))
- }
+ .takeIf { it.isNotEmpty() }
+ ?.let {
+ Timber.d("DELETED $it")
+ taskDeleter.delete(caldavDao.getTasks(caldavCalendar.uuid!!, it.toList()))
+ }
caldavCalendar.ctag = remoteCtag
Timber.d("UPDATE %s", caldavCalendar)
caldavDao.update(caldavCalendar)
diff --git a/app/src/main/java/org/tasks/caldav/iCalendar.kt b/app/src/main/java/org/tasks/caldav/iCalendar.kt
index b2dfa1f81..3fb78fd6f 100644
--- a/app/src/main/java/org/tasks/caldav/iCalendar.kt
+++ b/app/src/main/java/org/tasks/caldav/iCalendar.kt
@@ -38,7 +38,7 @@ class iCalendar @Inject constructor(
private val caldavDao: CaldavDao) {
companion object {
- private const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER"
+ const val APPLE_SORT_ORDER = "X-APPLE-SORT-ORDER"
private val IS_PARENT = { r: RelatedTo? ->
r!!.parameters.isEmpty || r.parameters.getParameter(Parameter.RELTYPE) === RelType.PARENT
diff --git a/app/src/main/java/org/tasks/data/CaldavAccount.kt b/app/src/main/java/org/tasks/data/CaldavAccount.kt
index 89238b411..c33e5d840 100644
--- a/app/src/main/java/org/tasks/data/CaldavAccount.kt
+++ b/app/src/main/java/org/tasks/data/CaldavAccount.kt
@@ -12,6 +12,7 @@ import org.tasks.activities.BaseListSettingsActivity
import org.tasks.caldav.CaldavCalendarSettingsActivity
import org.tasks.caldav.LocalListSettingsActivity
import org.tasks.etesync.EteSyncCalendarSettingsActivity
+import org.tasks.opentasks.OpenTasksListSettingsActivity
import org.tasks.security.KeyStoreEncryption
@Entity(tableName = "caldav_accounts")
@@ -85,9 +86,13 @@ class CaldavAccount : Parcelable {
val isEteSyncAccount: Boolean
get() = accountType == TYPE_ETESYNC
+ val isOpenTasks: Boolean
+ get() = accountType == TYPE_OPENTASKS
+
fun listSettingsClass(): Class = when(accountType) {
TYPE_ETESYNC -> EteSyncCalendarSettingsActivity::class.java
TYPE_LOCAL -> LocalListSettingsActivity::class.java
+ TYPE_OPENTASKS -> OpenTasksListSettingsActivity::class.java
else -> CaldavCalendarSettingsActivity::class.java
}
@@ -149,6 +154,7 @@ class CaldavAccount : Parcelable {
const val TYPE_CALDAV = 0
const val TYPE_ETESYNC = 1
const val TYPE_LOCAL = 2
+ const val TYPE_OPENTASKS = 3
@JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator {
override fun createFromParcel(source: Parcel): CaldavAccount? {
diff --git a/app/src/main/java/org/tasks/data/CaldavDao.kt b/app/src/main/java/org/tasks/data/CaldavDao.kt
index 5d417a737..79ea0b646 100644
--- a/app/src/main/java/org/tasks/data/CaldavDao.kt
+++ b/app/src/main/java/org/tasks/data/CaldavDao.kt
@@ -11,6 +11,8 @@ import com.todoroo.astrid.helper.UUIDHelper
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.tasks.R
+import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL
+import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.date.DateTimeUtils.toAppleEpoch
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.filters.CaldavFilters
@@ -158,7 +160,7 @@ SELECT EXISTS(SELECT 1
@Query("SELECT * FROM caldav_lists WHERE cdl_uuid = :uuid LIMIT 1")
abstract suspend fun getCalendar(uuid: String): CaldavCalendar?
- @Query("SELECT cd_object FROM caldav_tasks WHERE cd_calendar = :calendar")
+ @Query("SELECT cd_object FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_deleted = 0")
abstract suspend fun getObjects(calendar: String): List
suspend fun getTasks(calendar: String, objects: List): List =
@@ -167,6 +169,14 @@ SELECT EXISTS(SELECT 1
@Query("SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object IN (:objects)")
internal abstract suspend fun getTasksInternal(calendar: String, objects: List): List
+ @Query("""
+SELECT *
+FROM caldav_accounts
+WHERE cda_account_type = $TYPE_OPENTASKS
+ AND cda_uuid NOT IN (:accounts)
+ """)
+ abstract suspend fun findDeletedAccounts(accounts: List): List
+
@Query("SELECT * FROM caldav_lists WHERE cdl_account = :account AND cdl_url NOT IN (:urls)")
abstract suspend fun findDeletedCalendars(account: String, urls: List): List
diff --git a/app/src/main/java/org/tasks/data/OpenTaskDao.kt b/app/src/main/java/org/tasks/data/OpenTaskDao.kt
new file mode 100644
index 000000000..c5b8dd509
--- /dev/null
+++ b/app/src/main/java/org/tasks/data/OpenTaskDao.kt
@@ -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 = getLists().map { it.account!! }.distinct()
+
+ @Deprecated("add davx5/etesync accounts manually")
+ suspend fun accountCount(): Int = accounts().size
+
+ suspend fun getLists(): List = withContext(Dispatchers.IO) {
+ val calendars = ArrayList()
+ 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> = withContext(Dispatchers.IO) {
+ val items = ArrayList>()
+ 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 = withContext(Dispatchers.IO) {
+ val id = getId(caldavTask.remoteId)
+ val tags = ArrayList()
+ 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) = 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))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/data/TagDao.kt b/app/src/main/java/org/tasks/data/TagDao.kt
index e4ce4f868..ec076b892 100644
--- a/app/src/main/java/org/tasks/data/TagDao.kt
+++ b/app/src/main/java/org/tasks/data/TagDao.kt
@@ -30,7 +30,7 @@ abstract class TagDao {
abstract suspend fun delete(tags: List)
@Transaction
- open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: List): Boolean {
+ open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: List) {
val taskId = task.id
val existing = HashSet(tagDataDao.getTagDataForTask(taskId))
val selected = HashSet(current)
@@ -38,7 +38,6 @@ abstract class TagDao {
val removed = existing subtract selected
deleteTags(taskId, removed.map { td -> td.remoteId!! })
insert(task, added)
- return removed.isNotEmpty() || added.isNotEmpty()
}
suspend fun insert(task: Task, tags: Collection) {
diff --git a/app/src/main/java/org/tasks/data/TaskDao.kt b/app/src/main/java/org/tasks/data/TaskDao.kt
index 1145ceae5..9414bfb6e 100644
--- a/app/src/main/java/org/tasks/data/TaskDao.kt
+++ b/app/src/main/java/org/tasks/data/TaskDao.kt
@@ -68,7 +68,8 @@ abstract class TaskDao(private val database: Database) {
FROM tasks
INNER JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task
WHERE caldav_tasks.cd_calendar = :calendar
- AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)""")
+ AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)
+ ORDER BY created""")
abstract suspend fun getCaldavTasksToPush(calendar: String): List
@Query("SELECT * FROM TASKS "
diff --git a/app/src/main/java/org/tasks/filters/FilterProvider.kt b/app/src/main/java/org/tasks/filters/FilterProvider.kt
index e56ceb26b..b16933dbb 100644
--- a/app/src/main/java/org/tasks/filters/FilterProvider.kt
+++ b/app/src/main/java/org/tasks/filters/FilterProvider.kt
@@ -17,6 +17,7 @@ import org.tasks.billing.Inventory
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.*
import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL
+import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType
import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.HelpAndFeedback
@@ -225,7 +226,7 @@ class FilterProvider @Inject constructor(
caldavDao.getAccounts()
.ifEmpty { listOf(caldavDao.setupLocalAccount(context)) }
.filter { it.accountType != TYPE_LOCAL || preferences.getBoolean(R.string.p_lists_enabled, true) }
- .flatMap { caldavFilter(it, showCreate) }
+ .flatMap { caldavFilter(it, showCreate && it.accountType != TYPE_OPENTASKS) }
private suspend fun caldavFilter(account: CaldavAccount, showCreate: Boolean): List =
listOf(
diff --git a/app/src/main/java/org/tasks/injection/ProductionModule.kt b/app/src/main/java/org/tasks/injection/ProductionModule.kt
index 08b9c72f7..0691d7eaf 100644
--- a/app/src/main/java/org/tasks/injection/ProductionModule.kt
+++ b/app/src/main/java/org/tasks/injection/ProductionModule.kt
@@ -12,6 +12,7 @@ import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskListDao
+import org.tasks.data.OpenTaskDao
import org.tasks.db.Migrations
import org.tasks.jobs.WorkManager
import org.tasks.jobs.WorkManagerImpl
@@ -41,7 +42,8 @@ internal class ProductionModule {
@ApplicationContext context: Context,
preferences: Preferences,
googleTaskListDao: GoogleTaskListDao,
- caldavDao: CaldavDao): WorkManager {
- return WorkManagerImpl(context, preferences, googleTaskListDao, caldavDao)
+ caldavDao: CaldavDao,
+ openTaskDao: OpenTaskDao): WorkManager {
+ return WorkManagerImpl(context, preferences, googleTaskListDao, caldavDao, openTaskDao)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/jobs/SyncOpenTasksWork.kt b/app/src/main/java/org/tasks/jobs/SyncOpenTasksWork.kt
new file mode 100644
index 000000000..e197cf46b
--- /dev/null
+++ b/app/src/main/java/org/tasks/jobs/SyncOpenTasksWork.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/jobs/WorkManager.kt b/app/src/main/java/org/tasks/jobs/WorkManager.kt
index 6d2dd54a3..94947be7d 100644
--- a/app/src/main/java/org/tasks/jobs/WorkManager.kt
+++ b/app/src/main/java/org/tasks/jobs/WorkManager.kt
@@ -17,6 +17,7 @@ interface WorkManager {
fun eteSync(immediate: Boolean)
+ fun openTaskSync()
fun reverseGeocode(place: Place)
@@ -48,9 +49,11 @@ interface WorkManager {
const val TAG_SYNC_GOOGLE_TASKS = "tag_sync_google_tasks"
const val TAG_SYNC_CALDAV = "tag_sync_caldav"
const val TAG_SYNC_ETESYNC = "tag_sync_etesync"
+ const val TAG_SYNC_OPENTASK = "tag_sync_opentask"
const val TAG_BACKGROUND_SYNC_GOOGLE_TASKS = "tag_background_sync_google_tasks"
const val TAG_BACKGROUND_SYNC_CALDAV = "tag_background_sync_caldav"
const val TAG_BACKGROUND_SYNC_ETESYNC = "tag_background_sync_etesync"
+ const val TAG_BACKGROUND_SYNC_OPENTASKS = "tag_background_sync_opentasks"
const val TAG_REMOTE_CONFIG = "tag_remote_config"
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt b/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt
index 5d08ed264..f6db1603d 100644
--- a/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt
+++ b/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt
@@ -14,8 +14,10 @@ import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
+import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskListDao
+import org.tasks.data.OpenTaskDao
import org.tasks.data.Place
import org.tasks.date.DateTimeUtils.midnight
import org.tasks.date.DateTimeUtils.newDateTime
@@ -24,6 +26,7 @@ import org.tasks.jobs.WorkManager.Companion.REMOTE_CONFIG_INTERVAL_HOURS
import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_CALDAV
import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_ETESYNC
import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_GOOGLE_TASKS
+import org.tasks.jobs.WorkManager.Companion.TAG_BACKGROUND_SYNC_OPENTASKS
import org.tasks.jobs.WorkManager.Companion.TAG_BACKUP
import org.tasks.jobs.WorkManager.Companion.TAG_MIDNIGHT_REFRESH
import org.tasks.jobs.WorkManager.Companion.TAG_REFRESH
@@ -31,6 +34,7 @@ import org.tasks.jobs.WorkManager.Companion.TAG_REMOTE_CONFIG
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_CALDAV
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETESYNC
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_GOOGLE_TASKS
+import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_OPENTASK
import org.tasks.notifications.Throttle
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils
@@ -43,7 +47,8 @@ class WorkManagerImpl constructor(
private val context: Context,
private val preferences: Preferences,
private val googleTaskListDao: GoogleTaskListDao,
- private val caldavDao: CaldavDao): WorkManager {
+ private val caldavDao: CaldavDao,
+ private val openTaskDao: OpenTaskDao): WorkManager {
private val throttle = Throttle(200, 60000, "WORK")
private val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private val workManager = androidx.work.WorkManager.getInstance(context)
@@ -76,6 +81,9 @@ class WorkManagerImpl constructor(
override fun eteSync(immediate: Boolean) =
sync(immediate, TAG_SYNC_ETESYNC, SyncEteSyncWork::class.java)
+ override fun openTaskSync() =
+ sync(true, TAG_SYNC_OPENTASK, SyncOpenTasksWork::class.java, false)
+
@SuppressLint("EnqueueWork")
private fun sync(immediate: Boolean, tag: String, c: Class, requireNetwork: Boolean = true) {
Timber.d("sync(immediate = $immediate, $tag, $c, requireNetwork = $requireNetwork)")
@@ -139,6 +147,12 @@ class WorkManagerImpl constructor(
enabled && caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty(),
unmetered)
}
+ throttle.run {
+ scheduleBackgroundSync(
+ TAG_BACKGROUND_SYNC_OPENTASKS,
+ SyncOpenTasksWork::class.java,
+ enabled && (caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty() || openTaskDao.accountCount() > 0))
+ }
}
private fun scheduleBackgroundSync(
diff --git a/app/src/main/java/org/tasks/opentasks/OpenTaskAccountSettingsActivity.kt b/app/src/main/java/org/tasks/opentasks/OpenTaskAccountSettingsActivity.kt
new file mode 100644
index 000000000..0a26157c1
--- /dev/null
+++ b/app/src/main/java/org/tasks/opentasks/OpenTaskAccountSettingsActivity.kt
@@ -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"
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/opentasks/OpenTaskContentObserver.kt b/app/src/main/java/org/tasks/opentasks/OpenTaskContentObserver.kt
new file mode 100644
index 000000000..d3cb42abd
--- /dev/null
+++ b/app/src/main/java/org/tasks/opentasks/OpenTaskContentObserver.kt
@@ -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 =
+ listOf(TaskLists.getContentUri(authority),
+ Tasks.getContentUri(authority),
+ Properties.getContentUri(authority))
+ }
+}
diff --git a/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt b/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt
new file mode 100644
index 000000000..ca1955dc7
--- /dev/null
+++ b/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt
@@ -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) {}
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt
new file mode 100644
index 000000000..2ace6a4b3
--- /dev/null
+++ b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt
@@ -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> =
+ openTaskDao.getLists().groupBy { it.account!! }
+
+ private suspend fun sync(account: CaldavAccount, lists: List) {
+ 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) {
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/preferences/PermissionChecker.java b/app/src/main/java/org/tasks/preferences/PermissionChecker.java
index 41716274b..7f630e16b 100644
--- a/app/src/main/java/org/tasks/preferences/PermissionChecker.java
+++ b/app/src/main/java/org/tasks/preferences/PermissionChecker.java
@@ -15,7 +15,7 @@ public class PermissionChecker {
private final Context context;
@Inject
- PermissionChecker(@ApplicationContext Context context) {
+ public PermissionChecker(@ApplicationContext Context context) {
this.context = context;
}
diff --git a/app/src/main/java/org/tasks/preferences/fragments/Advanced.kt b/app/src/main/java/org/tasks/preferences/fragments/Advanced.kt
index 7d0d2d00c..e84846402 100644
--- a/app/src/main/java/org/tasks/preferences/fragments/Advanced.kt
+++ b/app/src/main/java/org/tasks/preferences/fragments/Advanced.kt
@@ -205,6 +205,10 @@ class Advanced : InjectingPreferenceFragment() {
.newDialog()
.setMessage(R.string.EPr_delete_task_data_warning)
.setPositiveButton(R.string.EPr_delete_task_data) { _, _ ->
+ context?.let {
+ it.deleteDatabase(database.name)
+ it.deleteDatabase("tasks.db") // opentasks
+ }
requireContext().deleteDatabase(database.name)
restart()
}
diff --git a/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt b/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt
index ff9afc14b..cdc88bc32 100644
--- a/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt
+++ b/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt
@@ -17,22 +17,26 @@ import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.caldav.CaldavAccountSettingsActivity
-import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskListDao
+import org.tasks.data.OpenTaskDao
+import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_DAVx5
+import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_ETESYNC
import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.jobs.WorkManager
+import org.tasks.opentasks.OpenTaskAccountSettingsActivity
import org.tasks.preferences.Preferences
-import org.tasks.sync.AddAccountDialog
+import org.tasks.sync.AddAccountDialog.Companion.newAccountDialog
import org.tasks.sync.SyncAdapters
import javax.inject.Inject
const val REQUEST_CALDAV_SETTINGS = 10013
const val REQUEST_GOOGLE_TASKS = 10014
private const val FRAG_TAG_ADD_ACCOUNT = "frag_tag_add_account"
+private const val REQUEST_ADD_ACCOUNT = 10015
@AndroidEntryPoint
class Synchronization : InjectingPreferenceFragment() {
@@ -43,6 +47,7 @@ class Synchronization : InjectingPreferenceFragment() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var syncAdapters: SyncAdapters
+ @Inject lateinit var openTaskDao: OpenTaskDao
override fun getPreferenceXml() = R.xml.preferences_synchronization
@@ -80,11 +85,25 @@ class Synchronization : InjectingPreferenceFragment() {
findPreference(R.string.add_account)
.setOnPreferenceClickListener {
- AddAccountDialog().show(parentFragmentManager, FRAG_TAG_ADD_ACCOUNT)
+ lifecycleScope.launch {
+ val accounts = openTaskDao.accounts().filter {
+ caldavDao.getAccountByUuid(it) == null
+ }
+ newAccountDialog(this@Synchronization, REQUEST_ADD_ACCOUNT, accounts)
+ .show(parentFragmentManager, FRAG_TAG_ADD_ACCOUNT)
+ }
false
}
}
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == REQUEST_ADD_ACCOUNT) {
+ refresh()
+ } else {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+
override fun onResume() {
super.onResume()
@@ -131,7 +150,7 @@ class Synchronization : InjectingPreferenceFragment() {
}
private suspend fun addCaldavAccounts(category: PreferenceCategory): Boolean {
- val accounts: List = caldavDao.getAccounts().filter {
+ val accounts = caldavDao.getAccounts().filter {
it.accountType != TYPE_LOCAL
}
for (account in accounts) {
@@ -139,18 +158,27 @@ class Synchronization : InjectingPreferenceFragment() {
preference.title = account.name
val error = account.error
if (isNullOrEmpty(error)) {
- preference.setSummary(
- if (account.isCaldavAccount) R.string.caldav else R.string.etesync
- )
+ preference.setSummary(when {
+ account.isCaldavAccount -> R.string.caldav
+ account.isEteSyncAccount
+ || (account.isOpenTasks
+ && account.uuid?.startsWith(ACCOUNT_TYPE_ETESYNC) == true) ->
+ R.string.etesync
+ account.isOpenTasks
+ && account.uuid?.startsWith(ACCOUNT_TYPE_DAVx5) == true ->
+ R.string.davx5
+ else -> 0
+ })
} else {
preference.summary = error
}
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- val intent = Intent(
- context,
- if (account.isCaldavAccount) CaldavAccountSettingsActivity::class.java
- else EteSyncAccountSettingsActivity::class.java
- )
+ val intent = Intent(context, when {
+ account.isCaldavAccount -> CaldavAccountSettingsActivity::class.java
+ account.isEteSyncAccount -> EteSyncAccountSettingsActivity::class.java
+ account.isOpenTasks -> OpenTaskAccountSettingsActivity::class.java
+ else -> throw IllegalArgumentException("Unexpected account type: $account")
+ })
intent.putExtra(BaseCaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, account)
startActivityForResult(intent, REQUEST_CALDAV_SETTINGS)
false
diff --git a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt
index 137b16bd6..d41e1657b 100644
--- a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt
+++ b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt
@@ -1,5 +1,6 @@
package org.tasks.sync
+import android.app.Activity.RESULT_OK
import android.app.Dialog
import android.content.Intent
import android.net.Uri
@@ -10,12 +11,20 @@ import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.caldav.CaldavAccountSettingsActivity
+import org.tasks.data.CaldavAccount
+import org.tasks.data.CaldavDao
+import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_DAVx5
+import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_ETESYNC
import org.tasks.dialogs.DialogBuilder
import org.tasks.etesync.EteSyncAccountSettingsActivity
+import org.tasks.jobs.WorkManager
import org.tasks.preferences.fragments.REQUEST_CALDAV_SETTINGS
import org.tasks.preferences.fragments.REQUEST_GOOGLE_TASKS
import org.tasks.themes.DrawableUtil
@@ -25,16 +34,36 @@ import javax.inject.Inject
class AddAccountDialog : DialogFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
+ @Inject lateinit var caldavDao: CaldavDao
+ @Inject lateinit var syncAdapters: SyncAdapters
+ @Inject lateinit var workManager: WorkManager
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val services = requireActivity().resources.getStringArray(R.array.synchronization_services)
- val descriptions = requireActivity().resources.getStringArray(R.array.synchronization_services_description)
- val typedArray = requireActivity().resources.obtainTypedArray(R.array.synchronization_services_icons)
- val icons = IntArray(typedArray.length())
- for (i in icons.indices) {
- icons[i] = typedArray.getResourceId(i, 0)
+ val services = requireActivity().resources.getStringArray(R.array.synchronization_services).toMutableList()
+ val descriptions = requireActivity().resources.getStringArray(R.array.synchronization_services_description).toMutableList()
+ val icons = arrayListOf(
+ R.drawable.ic_google,
+ R.drawable.ic_webdav_logo,
+ R.drawable.ic_etesync
+ )
+ val types = arrayListOf("", "", "")
+ requireArguments().getStringArrayList(EXTRA_ACCOUNTS)?.forEach { account ->
+ val (type, name) = account.split(":")
+ when (type) {
+ ACCOUNT_TYPE_DAVx5 -> {
+ services.add(name)
+ descriptions.add(getString(R.string.davx5))
+ types.add(ACCOUNT_TYPE_DAVx5)
+ icons.add(R.drawable.ic_davx5_icon_green_bg)
+ }
+ ACCOUNT_TYPE_ETESYNC -> {
+ services.add(name)
+ descriptions.add(getString(R.string.etesync))
+ types.add(ACCOUNT_TYPE_ETESYNC)
+ icons.add(R.drawable.ic_etesync)
+ }
+ }
}
- typedArray.recycle()
val adapter: ArrayAdapter = object : ArrayAdapter(
requireActivity(), R.layout.simple_list_item_2_themed, R.id.text1, services) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
@@ -54,17 +83,38 @@ class AddAccountDialog : DialogFragment() {
.setTitle(R.string.choose_synchronization_service)
.setSingleChoiceItems(adapter, -1) { dialog, which ->
when (which) {
- 0 -> activity?.startActivityForResult(
- Intent(activity, GtasksLoginActivity::class.java),
- REQUEST_GOOGLE_TASKS)
- 1 -> activity?.startActivityForResult(
- Intent(activity, CaldavAccountSettingsActivity::class.java),
- REQUEST_CALDAV_SETTINGS)
- 2 -> activity?.startActivityForResult(
- Intent(activity, EteSyncAccountSettingsActivity::class.java),
- REQUEST_CALDAV_SETTINGS)
+ 0 -> {
+ activity?.startActivityForResult(
+ Intent(activity, GtasksLoginActivity::class.java),
+ REQUEST_GOOGLE_TASKS)
+ dialog.dismiss()
+ }
+ 1 -> {
+ activity?.startActivityForResult(
+ Intent(activity, CaldavAccountSettingsActivity::class.java),
+ REQUEST_CALDAV_SETTINGS)
+ dialog.dismiss()
+ }
+ 2 -> {
+ activity?.startActivityForResult(
+ Intent(activity, EteSyncAccountSettingsActivity::class.java),
+ REQUEST_CALDAV_SETTINGS)
+ dialog.dismiss()
+ }
+ else -> {
+ lifecycleScope.launch {
+ caldavDao.insert(CaldavAccount().apply {
+ name = services[which]
+ uuid = "${types[which]}:${name}"
+ accountType = CaldavAccount.TYPE_OPENTASKS
+ })
+ syncAdapters.sync(true)
+ workManager.updateBackgroundSync()
+ dialog.dismiss()
+ targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, null)
+ }
+ }
}
- dialog.dismiss()
}
.setNeutralButton(R.string.help) { _, _ ->
activity?.startActivity(Intent(
@@ -75,4 +125,18 @@ class AddAccountDialog : DialogFragment() {
.show()
}
+ companion object {
+ private const val EXTRA_ACCOUNTS = "extra_accounts"
+
+ fun newAccountDialog(
+ targetFragment: Fragment, rc: Int, openTaskAccounts: List
+ ): AddAccountDialog {
+ val dialog = AddAccountDialog()
+ dialog.arguments = Bundle().apply {
+ putStringArrayList(EXTRA_ACCOUNTS, ArrayList(openTaskAccounts))
+ }
+ dialog.setTargetFragment(targetFragment, rc)
+ return dialog
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/sync/SyncAdapters.kt b/app/src/main/java/org/tasks/sync/SyncAdapters.kt
index a059ae143..01a81ddc9 100644
--- a/app/src/main/java/org/tasks/sync/SyncAdapters.kt
+++ b/app/src/main/java/org/tasks/sync/SyncAdapters.kt
@@ -5,6 +5,7 @@ import com.todoroo.astrid.data.Task
import kotlinx.coroutines.*
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
+import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
@@ -13,6 +14,7 @@ import org.tasks.jobs.WorkManager
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_CALDAV
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETESYNC
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_GOOGLE_TASKS
+import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_OPENTASK
import java.util.concurrent.Executors.newSingleThreadExecutor
import javax.inject.Inject
import javax.inject.Singleton
@@ -22,11 +24,13 @@ class SyncAdapters @Inject constructor(
workManager: WorkManager,
private val caldavDao: CaldavDao,
private val googleTaskDao: GoogleTaskDao,
- private val googleTaskListDao: GoogleTaskListDao) {
+ private val googleTaskListDao: GoogleTaskListDao,
+ private val openTaskDao: OpenTaskDao) {
private val scope = CoroutineScope(newSingleThreadExecutor().asCoroutineDispatcher() + SupervisorJob())
private val googleTasks = Debouncer(TAG_SYNC_GOOGLE_TASKS) { workManager.googleTaskSync(it) }
private val caldav = Debouncer(TAG_SYNC_CALDAV) { workManager.caldavSync(it) }
private val eteSync = Debouncer(TAG_SYNC_ETESYNC) { workManager.eteSync(it) }
+ private val opentasks = Debouncer(TAG_SYNC_OPENTASK) { workManager.openTaskSync() }
fun sync(task: Task, original: Task?) = scope.launch {
if (task.checkTransitory(SyncFlags.SUPPRESS_SYNC)) {
@@ -43,9 +47,16 @@ class SyncAdapters @Inject constructor(
if (caldavDao.isAccountType(task.id, TYPE_ETESYNC)) {
eteSync.sync(false)
}
+ if (caldavDao.isAccountType(task.id, TYPE_OPENTASKS)) {
+ opentasks.sync(false)
+ }
}
}
+ fun syncOpenTasks() = scope.launch {
+ opentasks.sync(false)
+ }
+
fun sync() {
sync(false)
}
@@ -54,6 +65,7 @@ class SyncAdapters @Inject constructor(
val googleTasksEnabled = async { isGoogleTaskSyncEnabled() }
val caldavEnabled = async { isCaldavSyncEnabled() }
val eteSyncEnabled = async { isEteSyncEnabled() }
+ val opentasksEnabled = async { isOpenTaskSyncEnabled() }
if (googleTasksEnabled.await()) {
googleTasks.sync(immediate)
@@ -67,6 +79,9 @@ class SyncAdapters @Inject constructor(
eteSync.sync(immediate)
}
+ if (opentasksEnabled.await()) {
+ opentasks.sync(immediate)
+ }
}
private suspend fun isGoogleTaskSyncEnabled() = googleTaskListDao.getAccounts().isNotEmpty()
@@ -75,4 +90,7 @@ class SyncAdapters @Inject constructor(
private suspend fun isEteSyncEnabled() = caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty()
+ private suspend fun isOpenTaskSyncEnabled() =
+ caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty()
+ || openTaskDao.accountCount() > 0
}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/time/DateTime.java b/app/src/main/java/org/tasks/time/DateTime.java
index c91be7482..8c44a9f8a 100644
--- a/app/src/main/java/org/tasks/time/DateTime.java
+++ b/app/src/main/java/org/tasks/time/DateTime.java
@@ -163,10 +163,18 @@ public class DateTime {
return startOfDay().setTime(hours, minutes, seconds, millisOfDay);
}
+ public long getOffset() {
+ return timeZone.getOffset(timestamp);
+ }
+
public long getMillis() {
return timestamp;
}
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
public int getMillisOfDay() {
Calendar calendar = getCalendar();
long millisOfDay =
@@ -338,6 +346,10 @@ public class DateTime {
return calendar;
}
+ public net.fortuna.ical4j.model.DateTime toDateTime() {
+ return timestamp == 0 ? null : new net.fortuna.ical4j.model.DateTime(timestamp);
+ }
+
public DateValue toDateValue() {
return timestamp == 0 ? null : new DateValueImpl(getYear(), getMonthOfYear(), getDayOfMonth());
}
diff --git a/app/src/main/res/drawable/ic_davx5_icon_green_bg.xml b/app/src/main/res/drawable/ic_davx5_icon_green_bg.xml
new file mode 100644
index 000000000..32f27b815
--- /dev/null
+++ b/app/src/main/res/drawable/ic_davx5_icon_green_bg.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/changelog.xml b/app/src/main/res/values/changelog.xml
index 413b0a1af..79d997ab4 100644
--- a/app/src/main/res/values/changelog.xml
+++ b/app/src/main/res/values/changelog.xml
@@ -1,7 +1,18 @@
- - Fix Google Task bugs
- - Join Tasks on Reddit: https://reddit.com/r/tasks
+ - 🚧 ALPHA VERSION 🚧
+ - PRO: DAVx⁵ support (alpha requires custom DAVx⁵ build)
+ - PRO: EteSync client support (alpha requires custom EteSync client build)
+ - ToDo Agenda integration
+ - Changed backstack behavior to follow Android conventions
+ - Major internal changes! Please report any bugs!
+ - Remove Mapbox tiles (Google Play only)
+ - Added \'Astrid manual sort\' information to backup file
+ - Bug fixes
+ - Performance improvements
+ - Security improvements
+ - Update translations
+ - Find Tasks on Reddit: https://reddit.com/r/tasks
diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml
index 78e29ebf0..0f8ca0704 100644
--- a/app/src/main/res/values/keys.xml
+++ b/app/src/main/res/values/keys.xml
@@ -4,10 +4,10 @@
These should not be translated
-->
-
Tasks Shortcut
CalDAV
EteSync
+ DAVx⁵
https://api.etesync.com
https://tasks.org/sync
@@ -336,6 +336,7 @@
sync_ongoing_google_tasks
sync_ongoing_caldav
sync_ongoing_etesync
+ sync_ongoing_opentasks
last_backup
show_description
show_full_description
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef713561d..c1c6801ca 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -635,4 +635,5 @@ File %1$s contained %2$s.\n\n
Lists
Reset sort order
Full access to Tasks database
+ Account not found