diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt index dd991811c..16b0a8092 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt @@ -13,6 +13,7 @@ import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.data.* +import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.data.Place.Companion.newPlace import org.tasks.preferences.Preferences import timber.log.Timber @@ -65,10 +66,11 @@ class TasksJsonImporter @Inject constructor( val version = input["version"].asInt val backupContainer = gson.fromJson(data, BackupContainer::class.java) backupContainer.tags?.forEach { tagData -> - tagData.setColor(themeToColor(context, version, tagData.getColor()!!)) - if (tagDataDao.getByUuid(tagData.remoteId!!) == null) { - tagDataDao.createNew(tagData) + findTagData(tagData)?.let { + return@forEach } + tagData.setColor(themeToColor(context, version, tagData.getColor()!!)) + tagDataDao.createNew(tagData) } backupContainer.googleTaskAccounts?.forEach { googleTaskAccount -> if (googleTaskListDao.getAccount(googleTaskAccount.account!!) == null) { @@ -98,10 +100,17 @@ class TasksJsonImporter @Inject constructor( } } backupContainer.caldavCalendars?.forEach { calendar -> - calendar.color = themeToColor(context, version, calendar.color) - if (caldavDao.getCalendarByUuid(calendar.uuid!!) == null) { - caldavDao.insert(calendar) + val account = caldavDao.getAccountByUuid(calendar.account!!)!! + when (account.accountType) { + TYPE_LOCAL -> if (caldavDao.getCalendarByUuid(calendar.uuid!!) != null) { + return@forEach + } + else -> if (caldavDao.getCalendarByUrl(calendar.account!!, calendar.url!!) != null) { + return@forEach + } } + calendar.color = themeToColor(context, version, calendar.color) + caldavDao.insert(calendar) } backupContainer.taskListMetadata?.forEach { tlm -> val id = tlm.filter.takeIf { it?.isNotBlank() == true } ?: tlm.tagUuid!! @@ -116,10 +125,19 @@ class TasksJsonImporter @Inject constructor( progressDialog, context.getString(R.string.import_progress_read, result.taskCount)) val task = backup.task - if (taskDao.fetch(task.uuid) != null) { - result.skipCount++ - return@forEach - } + taskDao.fetch(task.uuid) + ?.let { + result.skipCount++ + return@forEach + } + backup.caldavTasks + ?.filter { it.deleted == 0L } + ?.any { caldavDao.getCalendar(it.calendar!!) == null } + ?.takeIf { it } + ?.let { + result.skipCount++ + return@forEach + } task.suppressRefresh() task.suppressSync() taskDao.createNew(task) @@ -158,7 +176,9 @@ class TasksJsonImporter @Inject constructor( locationDao.insert(geofence) } for (tag in backup.tags) { + val tagData = findTagData(tag) ?: continue tag.task = taskId + tag.tagUid = tagData.remoteId tag.setTaskUid(taskUuid) tagDao.insert(tag) } @@ -214,6 +234,14 @@ class TasksJsonImporter @Inject constructor( return result } + private suspend fun findTagData(tagData: TagData) = + findTagData(tagData.remoteId!!, tagData.name!!) + + private suspend fun findTagData(tag: Tag) = findTagData(tag.tagUid!!, tag.name!!) + + private suspend fun findTagData(uid: String, name: String): TagData? = + tagDataDao.getByUuid(uid) ?: tagDataDao.getTagByName(name) + private fun themeToColor(context: Context, version: Int, color: Int) = if (version < Upgrader.V8_2) getAndroidColor(context, color) else color diff --git a/app/src/main/java/org/tasks/data/CaldavDao.kt b/app/src/main/java/org/tasks/data/CaldavDao.kt index 1c63a77f0..a593833a4 100644 --- a/app/src/main/java/org/tasks/data/CaldavDao.kt +++ b/app/src/main/java/org/tasks/data/CaldavDao.kt @@ -162,6 +162,9 @@ SELECT EXISTS(SELECT 1 @Query("SELECT * FROM caldav_lists ORDER BY cdl_name COLLATE NOCASE") abstract suspend fun getCalendars(): List + @Query("SELECT EXISTS(SELECT 1 FROM caldav_lists WHERE cdl_url IN (:urls))") + abstract suspend fun anyExist(urls: List): Boolean + @Query("SELECT * FROM caldav_lists WHERE cdl_uuid = :uuid LIMIT 1") abstract suspend fun getCalendar(uuid: String): CaldavCalendar? diff --git a/app/src/main/java/org/tasks/data/OpenTaskDao.kt b/app/src/main/java/org/tasks/data/OpenTaskDao.kt index 1fa12e439..8f095bbac 100644 --- a/app/src/main/java/org/tasks/data/OpenTaskDao.kt +++ b/app/src/main/java/org/tasks/data/OpenTaskDao.kt @@ -19,14 +19,22 @@ import org.tasks.caldav.iCalendar.Companion.APPLE_SORT_ORDER import timber.log.Timber import javax.inject.Inject -class OpenTaskDao @Inject constructor(@ApplicationContext context: Context) { - +class OpenTaskDao @Inject constructor( + @ApplicationContext context: Context, + private val caldavDao: CaldavDao +) { private val cr = context.contentResolver val authority = context.getString(R.string.opentasks_authority) private val tasks = Tasks.getContentUri(authority) private val properties = Properties.getContentUri(authority) - suspend fun accounts(): List = getLists().map { it.account!! }.distinct() + suspend fun newAccounts(): List = + getListsByAccount() + .newAccounts(caldavDao) + .map { it.key } + + suspend fun getListsByAccount(): Map> = + getLists().groupBy { it.account!! } suspend fun getLists(): List = withContext(Dispatchers.IO) { val calendars = ArrayList() @@ -238,6 +246,9 @@ class OpenTaskDao @Inject constructor(@ApplicationContext context: Context) { const val ACCOUNT_TYPE_DAVx5 = "bitfire.at.davdroid" const val ACCOUNT_TYPE_ETESYNC = "com.etesync.syncadapter" + suspend fun Map>.newAccounts(caldavDao: CaldavDao) = + filterNot { (_, lists) -> caldavDao.anyExist(lists.map { it.url!! }) } + fun Cursor.getString(columnName: String): String? = getString(getColumnIndex(columnName)) diff --git a/app/src/main/java/org/tasks/injection/ProductionModule.kt b/app/src/main/java/org/tasks/injection/ProductionModule.kt index 08b9c72f7..43387f604 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,9 @@ 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 index 730753833..126a797ff 100644 --- a/app/src/main/java/org/tasks/jobs/SyncOpenTasksWork.kt +++ b/app/src/main/java/org/tasks/jobs/SyncOpenTasksWork.kt @@ -9,6 +9,7 @@ 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 @@ -19,12 +20,14 @@ class SyncOpenTasksWork @WorkerInject constructor( localBroadcastManager: LocalBroadcastManager, preferences: Preferences, private val openTasksSynchronizer: OpenTasksSynchronizer, - private val caldavDao: CaldavDao + 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.newAccounts().isNotEmpty() override suspend fun doSync() { openTasksSynchronizer.sync() diff --git a/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt b/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt index 1baf240ce..4945e1c3c 100644 --- a/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt +++ b/app/src/main/java/org/tasks/jobs/WorkManagerImpl.kt @@ -17,6 +17,7 @@ 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 @@ -46,7 +47,8 @@ class WorkManagerImpl constructor( private val context: Context, private val preferences: Preferences, private val googleTaskListDao: GoogleTaskListDao, - private val caldavDao: CaldavDao + 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 @@ -150,7 +152,8 @@ class WorkManagerImpl constructor( scheduleBackgroundSync( TAG_BACKGROUND_SYNC_OPENTASKS, SyncOpenTasksWork::class.java, - enabled && caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty()) + caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty() + || openTaskDao.newAccounts().isNotEmpty()) } } diff --git a/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt index bf091f9c4..e474abccd 100644 --- a/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt +++ b/app/src/main/java/org/tasks/opentasks/OpenTasksSynchronizer.kt @@ -30,6 +30,7 @@ import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_ETESYNC import org.tasks.data.OpenTaskDao.Companion.getInt import org.tasks.data.OpenTaskDao.Companion.getLong import org.tasks.data.OpenTaskDao.Companion.getString +import org.tasks.data.OpenTaskDao.Companion.newAccounts import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.time.DateTime import org.tasks.time.DateTimeUtils.currentTimeMillis @@ -57,7 +58,14 @@ class OpenTasksSynchronizer @Inject constructor( private val cr = context.contentResolver suspend fun sync() { - val lists = getLists() + val lists = openTaskDao.getListsByAccount() + lists.newAccounts(caldavDao).forEach { (account, _) -> + caldavDao.insert(CaldavAccount().apply { + name = account.split(":")[1] + uuid = account + accountType = CaldavAccount.TYPE_OPENTASKS + }) + } caldavDao.getAccounts(CaldavAccount.TYPE_OPENTASKS).forEach { account -> if (!lists.containsKey(account.uuid)) { setError(account, context.getString(R.string.account_not_found)) @@ -69,9 +77,6 @@ class OpenTasksSynchronizer @Inject constructor( } } - 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 }) 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 cdc88bc32..4ed22e8b1 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt @@ -21,7 +21,6 @@ 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 @@ -47,7 +46,6 @@ 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 @@ -85,13 +83,8 @@ class Synchronization : InjectingPreferenceFragment() { findPreference(R.string.add_account) .setOnPreferenceClickListener { - lifecycleScope.launch { - val accounts = openTaskDao.accounts().filter { - caldavDao.getAccountByUuid(it) == null - } - newAccountDialog(this@Synchronization, REQUEST_ADD_ACCOUNT, accounts) - .show(parentFragmentManager, FRAG_TAG_ADD_ACCOUNT) - } + newAccountDialog(this@Synchronization, REQUEST_ADD_ACCOUNT) + .show(parentFragmentManager, FRAG_TAG_ADD_ACCOUNT) false } } diff --git a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt index d41e1657b..ae35a6778 100644 --- a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt +++ b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt @@ -1,8 +1,8 @@ package org.tasks.sync -import android.app.Activity.RESULT_OK import android.app.Dialog import android.content.Intent +import android.content.Intent.ACTION_VIEW import android.net.Uri import android.os.Bundle import android.view.View @@ -12,19 +12,12 @@ 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 @@ -34,36 +27,16 @@ 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).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) - } - } + 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) } + 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 { @@ -83,38 +56,19 @@ 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) - 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) - } - } + 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) + 3 -> activity?.startActivity( + Intent(ACTION_VIEW, Uri.parse("https://tasks.org/davx5"))) } + dialog.dismiss() } .setNeutralButton(R.string.help) { _, _ -> activity?.startActivity(Intent( @@ -126,15 +80,8 @@ class AddAccountDialog : DialogFragment() { } companion object { - private const val EXTRA_ACCOUNTS = "extra_accounts" - - fun newAccountDialog( - targetFragment: Fragment, rc: Int, openTaskAccounts: List - ): AddAccountDialog { + fun newAccountDialog(targetFragment: Fragment, rc: Int): AddAccountDialog { val dialog = AddAccountDialog() - dialog.arguments = Bundle().apply { - putStringArrayList(EXTRA_ACCOUNTS, ArrayList(openTaskAccounts)) - } dialog.setTargetFragment(targetFragment, rc) return dialog } diff --git a/app/src/main/java/org/tasks/sync/SyncAdapters.kt b/app/src/main/java/org/tasks/sync/SyncAdapters.kt index 17596b1ea..f58437a62 100644 --- a/app/src/main/java/org/tasks/sync/SyncAdapters.kt +++ b/app/src/main/java/org/tasks/sync/SyncAdapters.kt @@ -9,6 +9,7 @@ import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.CaldavDao import org.tasks.data.GoogleTaskDao import org.tasks.data.GoogleTaskListDao +import org.tasks.data.OpenTaskDao import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_CALDAV import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETESYNC @@ -23,7 +24,8 @@ 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) } @@ -88,5 +90,7 @@ class SyncAdapters @Inject constructor( private suspend fun isEteSyncEnabled() = caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty() - private suspend fun isOpenTaskSyncEnabled() = caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty() + private suspend fun isOpenTaskSyncEnabled() = + caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty() + || openTaskDao.newAccounts().isNotEmpty() } \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index f8b1f38e1..8c0f143dc 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -177,18 +177,21 @@ @string/gtasks_GPr_header @string/caldav @string/etesync + @string/davx5 @string/google_tasks_selection_description @string/caldav_selection_description @string/etesync_selection_description + @string/davx5_selection_description @drawable/ic_google @drawable/ic_webdav_logo @drawable/ic_etesync + @drawable/ic_davx5_icon_green_bg diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1c6801ca..a0dbff656 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -546,6 +546,7 @@ File %1$s contained %2$s.\n\n Basic service that synchronizes with your Google account Synchronization based on open internet standards Open source, end-to-end encrypted synchronization + Synchronize your tasks with the DAVx⁵ app Show advanced settings Requires an account with a CalDAV service provider or a self-hosted server. Find a service provider by visiting tasks.org/caldav Requires an account with EteSync.com or a self-hosted server