Copy etesync package to etebase, deprecate etesync

pull/1244/head
Alex Baker 4 years ago
parent f4e63d6b59
commit af2213d60f

@ -381,6 +381,10 @@
android:name=".etesync.EteSyncAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".etebase.EteBaseAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".opentasks.OpenTaskAccountSettingsActivity"
android:theme="@style/Tasks" />
@ -601,6 +605,14 @@
android:name=".etesync.EncryptionSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".etebase.EteBaseCalendarSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".etebase.EncryptionSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".preferences.MainPreferences"
android:theme="@style/Tasks" />

@ -6,4 +6,5 @@ object Constants {
const val SYNC_TYPE_DAVX5 = "davx5"
const val SYNC_TYPE_GOOGLE_TASKS = "google_tasks"
const val SYNC_TYPE_ETESYNC_OT = "etesync_ot"
const val SYNC_TYPE_ETEBASE = "etebase"
}

@ -108,7 +108,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
get() = !inventory.hasPro
@get:StringRes
protected abstract val description: Int
protected open val description = 0
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)

@ -12,6 +12,7 @@ import org.tasks.activities.BaseListSettingsActivity
import org.tasks.caldav.CaldavCalendarSettingsActivity
import org.tasks.caldav.LocalListSettingsActivity
import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_ETESYNC
import org.tasks.etebase.EteBaseCalendarSettingsActivity
import org.tasks.etesync.EteSyncCalendarSettingsActivity
import org.tasks.opentasks.OpenTasksListSettingsActivity
import org.tasks.security.KeyStoreEncryption
@ -84,9 +85,13 @@ class CaldavAccount : Parcelable {
val isCaldavAccount: Boolean
get() = accountType == TYPE_CALDAV
@Deprecated("use etebase")
val isEteSyncAccount: Boolean
get() = accountType == TYPE_ETESYNC
val isEteBaseAccount: Boolean
get() = accountType == TYPE_ETEBASE
val isOpenTasks: Boolean
get() = accountType == TYPE_OPENTASKS
@ -100,6 +105,7 @@ class CaldavAccount : Parcelable {
TYPE_ETESYNC -> EteSyncCalendarSettingsActivity::class.java
TYPE_LOCAL -> LocalListSettingsActivity::class.java
TYPE_OPENTASKS -> OpenTasksListSettingsActivity::class.java
TYPE_ETEBASE -> EteBaseCalendarSettingsActivity::class.java
else -> CaldavCalendarSettingsActivity::class.java
}
@ -159,10 +165,11 @@ class CaldavAccount : Parcelable {
companion object {
const val TYPE_CALDAV = 0
const val TYPE_ETESYNC = 1
@Deprecated("use etebase") const val TYPE_ETESYNC = 1
const val TYPE_LOCAL = 2
const val TYPE_OPENTASKS = 3
const val TYPE_TASKS = 4
const val TYPE_ETEBASE = 5
fun String?.openTaskType(): String? = this?.split(":")?.get(0)

@ -0,0 +1,17 @@
package org.tasks.etebase
import androidx.core.util.Pair
import androidx.hilt.lifecycle.ViewModelInject
import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.ui.CompletableViewModel
class AddEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun addAccount(url: String, username: String, password: String) {
run {
client.setForeground()
val token = client.forUrl(url, username, null, null).getToken(password)
Pair.create(client.forUrl(url, username, null, token!!).userInfo(), token)
}
}
}

@ -0,0 +1,12 @@
package org.tasks.etebase
import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
class CreateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<String?>() {
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {
run { client.forAccount(account).makeCollection(name, color) }
}
}

@ -0,0 +1,15 @@
package org.tasks.etebase
import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
class CreateUserInfoViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String>() {
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {
run {
client.forAccount(caldavAccount).createUserInfo(derivedKey)
derivedKey
}
}
}

@ -0,0 +1,13 @@
package org.tasks.etebase
import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.ui.ActionViewModel
class DeleteCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : ActionViewModel() {
suspend fun deleteCalendar(account: CaldavAccount, calendar: CaldavCalendar) {
run { client.forAccount(account).deleteCollection(calendar) }
}
}

@ -0,0 +1,188 @@
package org.tasks.etebase
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import butterknife.ButterKnife
import butterknife.OnTextChanged
import com.etesync.journalmanager.Constants.Companion.CURRENT_VERSION
import com.etesync.journalmanager.Crypto.CryptoManager
import com.etesync.journalmanager.Crypto.deriveKey
import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException
import com.etesync.journalmanager.UserInfoManager
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.CaldavAccount
import org.tasks.databinding.ActivityEtesyncEncryptionSettingsBinding
import org.tasks.injection.ThemedInjectingAppCompatActivity
import org.tasks.security.KeyStoreEncryption
import org.tasks.ui.DisplayableException
import java.net.ConnectException
import javax.inject.Inject
@AndroidEntryPoint
class EncryptionSettingsActivity : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var encryption: KeyStoreEncryption
private lateinit var binding: ActivityEtesyncEncryptionSettingsBinding
private var userInfo: UserInfoManager.UserInfo? = null
private var caldavAccount: CaldavAccount? = null
private val createUserInfoViewModel: CreateUserInfoViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEtesyncEncryptionSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
ButterKnife.bind(this)
val intent = intent
caldavAccount = intent.getParcelableExtra(EXTRA_ACCOUNT)
userInfo = intent.getSerializableExtra(EXTRA_USER_INFO) as UserInfoManager.UserInfo
if (userInfo == null) {
binding.description.visibility = View.VISIBLE
binding.repeatEncryptionPasswordLayout.visibility = View.VISIBLE
}
val toolbar = binding.toolbar.toolbar
toolbar.title = if (caldavAccount == null) getString(R.string.add_account) else caldavAccount!!.name
toolbar.navigationIcon = getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener { save() }
toolbar.inflateMenu(R.menu.menu_help)
toolbar.setOnMenuItemClickListener(this)
themeColor.apply(toolbar)
createUserInfoViewModel.observe(this, { returnDerivedKey(it) }, this::requestFailed)
if (createUserInfoViewModel.inProgress) {
showProgressIndicator()
}
}
private fun showProgressIndicator() {
binding.progressBar.progressBar.visibility = View.VISIBLE
}
private fun hideProgressIndicator() {
binding.progressBar.progressBar.visibility = View.GONE
}
private fun requestInProgress() = binding.progressBar.progressBar.visibility == View.VISIBLE
private fun returnDerivedKey(derivedKey: String) {
hideProgressIndicator()
val result = Intent()
result.putExtra(EXTRA_DERIVED_KEY, derivedKey)
setResult(Activity.RESULT_OK, result)
finish()
return
}
private fun save() = lifecycleScope.launch {
if (requestInProgress()) {
return@launch
}
val encryptionPassword = newEncryptionPassword
val derivedKey = caldavAccount!!.getEncryptionPassword(encryption)
if (isNullOrEmpty(encryptionPassword) && isNullOrEmpty(derivedKey)) {
binding.encryptionPasswordLayout.error = getString(R.string.encryption_password_required)
return@launch
}
if (userInfo == null) {
val repeatEncryptionPassword = binding.repeatEncryptionPassword.text.toString().trim { it <= ' ' }
if (encryptionPassword != repeatEncryptionPassword) {
binding.repeatEncryptionPasswordLayout.error = getString(R.string.passwords_do_not_match)
return@launch
}
}
val key = if (isNullOrEmpty(encryptionPassword)) derivedKey else deriveKey(caldavAccount!!.username!!, encryptionPassword)
val cryptoManager: CryptoManager
cryptoManager = try {
val version = if (userInfo == null) CURRENT_VERSION else userInfo!!.version!!.toInt()
CryptoManager(version, key, "userInfo")
} catch (e: VersionTooNewException) {
requestFailed(e)
return@launch
} catch (e: IntegrityException) {
requestFailed(e)
return@launch
}
if (userInfo == null) {
showProgressIndicator()
createUserInfoViewModel.createUserInfo(caldavAccount!!, key)
} else {
try {
userInfo!!.verify(cryptoManager)
returnDerivedKey(key)
} catch (e: IntegrityException) {
binding.encryptionPasswordLayout.error = getString(R.string.encryption_password_wrong)
}
}
}
private fun requestFailed(t: Throwable) {
hideProgressIndicator()
when (t) {
is HttpException -> showSnackbar(t.message)
is DisplayableException -> showSnackbar(t.resId)
is ConnectException -> showSnackbar(R.string.network_error)
else -> showSnackbar(R.string.error_adding_account, t.message!!)
}
}
private fun showSnackbar(resId: Int, vararg formatArgs: Any) =
showSnackbar(getString(resId, *formatArgs))
private fun showSnackbar(message: String?) =
newSnackbar(message).show()
private fun newSnackbar(message: String?): Snackbar {
val snackbar = Snackbar.make(binding.rootLayout, message!!, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar
.view
.setBackgroundColor(getColor(R.color.snackbar_background))
return snackbar
}
@OnTextChanged(R.id.repeat_encryption_password)
fun onRpeatEncryptionPasswordChanged() {
binding.repeatEncryptionPasswordLayout.error = null
}
@OnTextChanged(R.id.encryption_password)
fun onEncryptionPasswordChanged() {
binding.encryptionPasswordLayout.error = null
}
private val newEncryptionPassword: String
get() = binding.encryptionPassword.text.toString().trim { it <= ' ' }
override fun finish() {
if (!requestInProgress()) {
super.finish()
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return if (item.itemId == R.id.menu_help) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_etesync))))
true
} else {
onOptionsItemSelected(item)
}
}
companion object {
const val EXTRA_USER_INFO = "extra_user_info"
const val EXTRA_ACCOUNT = "extra_account"
const val EXTRA_DERIVED_KEY = "extra_derived_key"
}
}

@ -0,0 +1,183 @@
package org.tasks.etebase
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.core.util.Pair
import androidx.lifecycle.lifecycleScope
import butterknife.OnCheckedChanged
import com.etesync.journalmanager.Crypto.CryptoManager
import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException
import com.etesync.journalmanager.UserInfoManager
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Constants
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var eteBaseClient: EteBaseClient
private val addAccountViewModel: AddEteBaseAccountViewModel by viewModels()
private val updateAccountViewModel: UpdateEteBaseAccountViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.repeat.visibility = View.GONE
binding.showAdvanced.visibility = View.VISIBLE
updateUrlVisibility()
}
override fun onResume() {
super.onResume()
if (!isFinishing) {
addAccountViewModel.observe(this, this::addAccount, this::requestFailed)
updateAccountViewModel.observe(this, this::updateAccount, this::requestFailed)
}
}
override fun onPause() {
super.onPause()
addAccountViewModel.removeObserver(this)
updateAccountViewModel.removeObserver(this)
}
override val description: Int
get() = R.string.etesync_account_description
private suspend fun addAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
caldavAccount = CaldavAccount()
caldavAccount!!.accountType = CaldavAccount.TYPE_ETEBASE
caldavAccount!!.uuid = UUIDHelper.newUUID()
applyTo(caldavAccount!!, userInfoAndToken)
}
private suspend fun updateAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
caldavAccount!!.error = ""
applyTo(caldavAccount!!, userInfoAndToken)
}
private suspend fun applyTo(account: CaldavAccount, userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
hideProgressIndicator()
account.name = newName
account.url = newURL
account.username = newUsername
val token = userInfoAndToken.second
if (token != account.getPassword(encryption)) {
account.password = encryption.encrypt(token!!)
}
val userInfo = userInfoAndToken.first
if (testUserInfo(userInfo)) {
saveAccountAndFinish()
} else {
val intent = Intent(this, EncryptionSettingsActivity::class.java)
intent.putExtra(EncryptionSettingsActivity.EXTRA_USER_INFO, userInfo)
intent.putExtra(EncryptionSettingsActivity.EXTRA_ACCOUNT, account)
startActivityForResult(intent, REQUEST_ENCRYPTION_PASSWORD)
}
}
private fun testUserInfo(userInfo: UserInfoManager.UserInfo?): Boolean {
val encryptionKey = caldavAccount!!.getEncryptionPassword(encryption)
if (userInfo != null && !isNullOrEmpty(encryptionKey)) {
try {
val cryptoManager = CryptoManager(userInfo.version!!.toInt(), encryptionKey, "userInfo")
userInfo.verify(cryptoManager)
return true
} catch (e: IntegrityException) {
Timber.e(e)
} catch (e: VersionTooNewException) {
Timber.e(e)
}
}
return false
}
@OnCheckedChanged(R.id.show_advanced)
fun toggleUrl() {
updateUrlVisibility()
}
private fun updateUrlVisibility() {
binding.urlLayout.visibility = if (binding.showAdvanced.isChecked) View.VISIBLE else View.GONE
}
override fun needsValidation(): Boolean {
return super.needsValidation() || isNullOrEmpty(caldavAccount!!.encryptionKey)
}
override suspend fun addAccount(url: String, username: String, password: String) =
addAccountViewModel.addAccount(url, username, password)
override suspend fun updateAccount(url: String, username: String, password: String) =
updateAccountViewModel.updateAccount(
url,
username,
if (PASSWORD_MASK == password) null else password,
caldavAccount!!.getPassword(encryption))
override suspend fun updateAccount() {
caldavAccount!!.name = newName
saveAccountAndFinish()
}
override val newURL: String
get() {
val url = super.newURL
return if (isNullOrEmpty(url)) getString(R.string.etesync_url) else url // TODO: change to etebase url
}
override val newPassword: String
get() = binding.password.text.toString().trim { it <= ' ' }
override val helpUrl: String
get() = getString(R.string.url_etesync)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_ENCRYPTION_PASSWORD) {
if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
val key = data!!.getStringExtra(EncryptionSettingsActivity.EXTRA_DERIVED_KEY)!!
caldavAccount!!.encryptionKey = encryption.encrypt(key)
saveAccountAndFinish()
}
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private suspend fun saveAccountAndFinish() {
if (caldavAccount!!.id == Task.NO_ID) {
caldavDao.insert(caldavAccount!!)
firebase.logEvent(
R.string.event_sync_add_account,
R.string.param_type to Constants.SYNC_TYPE_ETEBASE
)
} else {
caldavDao.update(caldavAccount!!)
}
setResult(Activity.RESULT_OK)
finish()
}
override suspend fun removeAccount() {
caldavAccount?.let { eteBaseClient.forAccount(it).invalidateToken() }
super.removeAccount()
}
companion object {
private const val REQUEST_ENCRYPTION_PASSWORD = 10101
}
}

@ -0,0 +1,33 @@
package org.tasks.etebase
import android.os.Bundle
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
@AndroidEntryPoint
class EteBaseCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
private val createCalendarViewModel: CreateCalendarViewModel by viewModels()
private val deleteCalendarViewModel: DeleteCalendarViewModel by viewModels()
private val updateCalendarViewModel: UpdateCalendarViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
createCalendarViewModel.observe(this, this::createSuccessful, this::requestFailed)
deleteCalendarViewModel.observe(this, this::onDeleted, this::requestFailed)
updateCalendarViewModel.observe(this, { updateCalendar() }, this::requestFailed)
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) =
createCalendarViewModel.createCalendar(caldavAccount, name, color)
override suspend fun updateNameAndColor(
account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) =
updateCalendarViewModel.updateCalendar(account, calendar, name, color)
override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) =
deleteCalendarViewModel.deleteCalendar(caldavAccount, caldavCalendar)
}

@ -0,0 +1,277 @@
package org.tasks.etebase
import android.content.Context
import androidx.core.util.Pair
import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.*
import com.etesync.journalmanager.Constants.Companion.CURRENT_VERSION
import com.etesync.journalmanager.Crypto.AsymmetricKeyPair
import com.etesync.journalmanager.Crypto.CryptoManager
import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException
import com.etesync.journalmanager.JournalManager.Journal
import com.etesync.journalmanager.UserInfoManager.UserInfo.Companion.generate
import com.etesync.journalmanager.model.CollectionInfo
import com.etesync.journalmanager.model.CollectionInfo.Companion.fromJson
import com.etesync.journalmanager.model.SyncEntry
import com.etesync.journalmanager.util.TokenAuthenticator
import com.google.common.collect.Lists
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.MemoryCookieStore
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import timber.log.Timber
import java.io.IOException
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
class EteBaseClient {
private val encryption: KeyStoreEncryption
private val preferences: Preferences
private val interceptor: DebugNetworkInterceptor
private val username: String?
private val token: String?
private val encryptionPassword: String?
private val httpClient: OkHttpClient?
private val httpUrl: HttpUrl?
private val context: Context
private val journalManager: JournalManager?
private var foreground = false
@Inject
constructor(
@ApplicationContext context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
username = null
token = null
encryptionPassword = null
httpClient = null
httpUrl = null
journalManager = null
}
private constructor(
context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor,
url: String?,
username: String?,
encryptionPassword: String?,
token: String?,
foreground: Boolean) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
this.username = username
this.encryptionPassword = encryptionPassword
this.token = token
this.foreground = foreground
val customCertManager = CustomCertManager(context)
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(customCertManager), null)
val builder = OkHttpClient()
.newBuilder()
.addNetworkInterceptor(TokenAuthenticator(null, token))
.cookieJar(MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.socketFactory, customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
if (preferences.isFlipperEnabled) {
interceptor.apply(builder)
}
httpClient = builder.build()
httpUrl = url?.toHttpUrlOrNull()
journalManager = JournalManager(httpClient, httpUrl!!)
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl(
account.url,
account.username,
account.getEncryptionPassword(encryption),
account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) {
EteBaseClient(
context,
encryption,
preferences,
interceptor,
url,
username,
encryptionPassword,
token,
foreground)
}
@Throws(IOException::class, Exceptions.HttpException::class)
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!)
}
@Throws(Exceptions.HttpException::class)
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) {
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!)
userInfoManager.fetch(username!!)
}
@Throws(VersionTooNewException::class, IntegrityException::class)
fun getCrypto(userInfo: UserInfoManager.UserInfo?, journal: Journal): CryptoManager {
if (journal.key == null) {
return CryptoManager(journal.version, encryptionPassword!!, journal.uid!!)
}
if (userInfo == null) {
throw RuntimeException("Missing userInfo")
}
val cryptoManager = CryptoManager(userInfo.version!!.toInt(), encryptionPassword!!, "userInfo")
val keyPair = AsymmetricKeyPair(userInfo.getContent(cryptoManager)!!, userInfo.pubkey!!)
return CryptoManager(journal.version, keyPair, journal.key!!)
}
private fun convertJournalToCollection(userInfo: UserInfoManager.UserInfo?, journal: Journal): CollectionInfo? {
return try {
val cryptoManager = getCrypto(userInfo, journal)
journal.verify(cryptoManager)
val collection = fromJson(journal.getContent(cryptoManager))
collection.updateFromJournal(journal)
collection
} catch (e: IntegrityException) {
Timber.e(e)
null
} catch (e: VersionTooNewException) {
Timber.e(e)
null
}
}
@Throws(Exceptions.HttpException::class)
suspend fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> = withContext(Dispatchers.IO) {
val result: MutableMap<Journal, CollectionInfo> = HashMap()
for (journal in journalManager!!.list()) {
val collection = convertJournalToCollection(userInfo, journal)
if (collection != null) {
if (TYPE_TASKS == collection.type) {
Timber.v("Found %s", collection)
result[journal] = collection
} else {
Timber.v("Ignoring %s", collection)
}
}
}
result
}
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
suspend fun getSyncEntries(
userInfo: UserInfoManager.UserInfo?,
journal: Journal,
calendar: CaldavCalendar,
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) {
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
val crypto = getCrypto(userInfo, journal)
var journalEntries: List<JournalEntryManager.Entry>
do {
journalEntries = journalEntryManager.list(crypto, calendar.ctag, MAX_FETCH)
callback.invoke(journalEntries.map {
Pair.create(it, SyncEntry.fromJournalEntry(crypto, it))
})
} while (journalEntries.size >= MAX_FETCH)
}
@Throws(Exceptions.HttpException::class)
suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) {
var remoteCtag = remoteCtag
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
for (partition in Lists.partition(entries!!, MAX_PUSH)) {
journalEntryManager.create(partition, remoteCtag)
remoteCtag = partition[partition.size - 1].uid
}
}
fun setForeground() {
foreground = true
}
suspend fun invalidateToken() = withContext(Dispatchers.IO) {
try {
JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!)
} catch (e: Exception) {
Timber.e(e)
}
}
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
suspend fun makeCollection(name: String?, color: Int): String = withContext(Dispatchers.IO) {
val uid = Journal.genUid()
val collectionInfo = CollectionInfo()
collectionInfo.displayName = name
collectionInfo.type = TYPE_TASKS
collectionInfo.uid = uid
collectionInfo.selected = true
collectionInfo.color = if (color == 0) null else color
val crypto = CryptoManager(collectionInfo.version, encryptionPassword!!, uid)
journalManager!!.create(Journal(crypto, collectionInfo.toJson(), uid))
uid
}
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? = withContext(Dispatchers.IO) {
val uid = calendar.url
val journal = journalManager!!.fetch(uid!!)
val userInfo = userInfo()
val crypto = getCrypto(userInfo, journal)
val collectionInfo = convertJournalToCollection(userInfo, journal)
collectionInfo!!.displayName = name
collectionInfo.color = if (color == 0) null else color
journalManager.update(Journal(crypto, collectionInfo.toJson(), uid))
uid
}
@Throws(Exceptions.HttpException::class)
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) {
journalManager!!.delete(Journal.fakeWithUid(calendar.url!!))
}
@Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class)
suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) {
val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo")
val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!)
UserInfoManager(httpClient!!, httpUrl!!).create(userInfo)
}
companion object {
private const val TYPE_TASKS = "TASKS"
private const val MAX_FETCH = 50
private const val MAX_PUSH = 30
}
}

@ -0,0 +1,224 @@
package org.tasks.etebase
import android.content.Context
import androidx.core.util.Pair
import at.bitfire.ical4android.ICalendar.Companion.prodId
import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException
import com.etesync.journalmanager.JournalEntryManager
import com.etesync.journalmanager.JournalEntryManager.Entry.Companion.getFakeWithUid
import com.etesync.journalmanager.JournalManager.Journal
import com.etesync.journalmanager.UserInfoManager
import com.etesync.journalmanager.model.SyncEntry
import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext
import net.fortuna.ical4j.model.property.ProdId
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTaskContainer
import timber.log.Timber
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.inject.Inject
import kotlin.collections.HashSet
class EteBaseSynchronizer @Inject constructor(
@param:ApplicationContext private val context: Context,
private val caldavDao: CaldavDao,
private val localBroadcastManager: LocalBroadcastManager,
private val taskDeleter: TaskDeleter,
private val inventory: Inventory,
private val client: EteBaseClient,
private val iCal: iCalendar) {
companion object {
init {
prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")
}
}
suspend fun sync(account: CaldavAccount) {
Thread.currentThread().contextClassLoader = context.classLoader
if (!inventory.hasPro) {
setError(account, context.getString(R.string.requires_pro_subscription))
return
}
if (isNullOrEmpty(account.password)) {
setError(account, context.getString(R.string.password_required))
return
}
if (isNullOrEmpty(account.encryptionKey)) {
setError(account, context.getString(R.string.encryption_password_required))
return
}
try {
synchronize(account)
} catch (e: KeyManagementException) {
setError(account, e.message)
} catch (e: NoSuchAlgorithmException) {
setError(account, e.message)
} catch (e: Exceptions.HttpException) {
setError(account, e.message)
} catch (e: IntegrityException) {
setError(account, e.message)
} catch (e: VersionTooNewException) {
setError(account, e.message)
}
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
private suspend fun synchronize(account: CaldavAccount) {
val client = client.forAccount(account)
val userInfo = client.userInfo()
val resources = client.getCalendars(userInfo)
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()
Timber.d("Found uids: %s", uids)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, uids.toList())) {
taskDeleter.delete(calendar)
}
for ((key, collection) in resources) {
val uid = collection.uid
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, uid!!)
val colorInt = collection.color
val color = colorInt ?: 0
if (calendar == null) {
calendar = CaldavCalendar()
calendar.name = collection.displayName
calendar.account = account.uuid
calendar.url = collection.uid
calendar.uuid = UUIDHelper.newUUID()
calendar.color = color
caldavDao.insert(calendar)
} else {
if (calendar.name != collection.displayName
|| calendar.color != color) {
calendar.name = collection.displayName
calendar.color = color
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
}
}
sync(client, userInfo!!, calendar, key)
}
setError(account, "")
}
private suspend fun setError(account: CaldavAccount, message: String?) {
account.error = message
caldavDao.update(account)
localBroadcastManager.broadcastRefreshList()
if (!isNullOrEmpty(message)) {
Timber.e(message)
}
}
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
private suspend fun sync(
client: EteBaseClient,
userInfo: UserInfoManager.UserInfo,
caldavCalendar: CaldavCalendar,
journal: Journal) {
Timber.d("sync(%s)", caldavCalendar)
val localChanges = HashMap<String?, CaldavTaskContainer>()
for (task in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) {
localChanges[task.remoteId] = task
}
var remoteCtag = journal.lastUid
if (isNullOrEmpty(remoteCtag) || remoteCtag != caldavCalendar.ctag) {
Timber.v("Applying remote changes")
client.getSyncEntries(userInfo, journal, caldavCalendar) {
applyEntries(caldavCalendar, it, localChanges.keys)
}
} else {
Timber.d("%s up to date", caldavCalendar.name)
}
val changes: MutableList<SyncEntry> = ArrayList()
for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) {
val vtodo = task.vtodo
if (!isNullOrEmpty(vtodo)) {
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE))
}
}
for (task in localChanges.values) {
val vtodo = task.vtodo
val existingTask = !isNullOrEmpty(vtodo)
if (task.isDeleted) {
if (existingTask) {
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE))
}
} else {
changes.add(
SyncEntry(
String(iCal.toVtodo(task.caldavTask, task.task)),
if (existingTask) SyncEntry.Actions.CHANGE else SyncEntry.Actions.ADD))
}
}
remoteCtag = caldavCalendar.ctag
val crypto = client.getCrypto(userInfo, journal)
val updates: MutableList<Pair<JournalEntryManager.Entry, SyncEntry>> = ArrayList()
var previous: JournalEntryManager.Entry? = if (isNullOrEmpty(remoteCtag)) null else getFakeWithUid(remoteCtag!!)
for (syncEntry in changes) {
val entry = JournalEntryManager.Entry()
entry.update(crypto, syncEntry.toJson(), previous)
updates.add(Pair.create(entry, syncEntry))
previous = entry
}
if (updates.size > 0) {
Timber.v("Pushing local changes")
client.pushEntries(journal, updates.mapNotNull { it.first }, remoteCtag)
Timber.v("Applying local changes")
applyEntries(caldavCalendar, updates, HashSet())
}
Timber.d("UPDATE %s", caldavCalendar)
caldavDao.update(caldavCalendar)
caldavDao.updateParents(caldavCalendar.uuid!!)
localBroadcastManager.broadcastRefresh()
}
private suspend fun applyEntries(
caldavCalendar: CaldavCalendar,
syncEntries: List<Pair<JournalEntryManager.Entry, SyncEntry>>,
dirty: MutableSet<String?>) {
for (entry in syncEntries) {
val journalEntry = entry.first
val syncEntry = entry.second
val action = syncEntry!!.action
val vtodo = syncEntry.content
Timber.v("%s: %s", action, vtodo)
val task = fromVtodo(vtodo) ?: continue
val remoteId = task.uid
val caldavTask = caldavDao.getTaskByRemoteId(caldavCalendar.uuid!!, remoteId!!)
when (action) {
SyncEntry.Actions.ADD, SyncEntry.Actions.CHANGE -> if (dirty.contains(remoteId)) {
caldavTask!!.vtodo = vtodo
caldavDao.update(caldavTask)
} else {
iCal.fromVtodo(caldavCalendar, caldavTask, task, vtodo, null, null)
}
SyncEntry.Actions.DELETE -> {
dirty.remove(remoteId)
if (caldavTask != null) {
if (caldavTask.isDeleted()) {
caldavDao.delete(caldavTask)
} else {
taskDeleter.delete(caldavTask.task)
}
}
}
}
caldavCalendar.ctag = journalEntry!!.uid
caldavDao.update(caldavCalendar)
}
}
}

@ -0,0 +1,13 @@
package org.tasks.etebase
import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.ui.CompletableViewModel
class UpdateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String?>() {
suspend fun updateCalendar(account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) {
run { client.forAccount(account).updateCollection(calendar, name, color) }
}
}

@ -0,0 +1,22 @@
package org.tasks.etebase
import androidx.core.util.Pair
import androidx.hilt.lifecycle.ViewModelInject
import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.Strings.isNullOrEmpty
import org.tasks.ui.CompletableViewModel
class UpdateEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun updateAccount(url: String, user: String, pass: String?, token: String) {
run {
client.setForeground()
if (isNullOrEmpty(pass)) {
Pair.create(client.forUrl(url, user, null, token).userInfo(), token)
} else {
val newToken = client.forUrl(url, user, null, null).getToken(pass)
Pair.create(client.forUrl(url, user, null, newToken).userInfo(), newToken)
}
}
}
}

@ -5,6 +5,7 @@ import androidx.hilt.lifecycle.ViewModelInject
import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.ui.CompletableViewModel
@Deprecated("use etebase")
class AddEteSyncAccountViewModel @ViewModelInject constructor(
private val client: EteSyncClient): CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun addAccount(url: String, username: String, password: String) {

@ -4,6 +4,7 @@ import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
@Deprecated("use etebase")
class CreateCalendarViewModel @ViewModelInject constructor(
private val client: EteSyncClient) : CompletableViewModel<String?>() {
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {

@ -4,6 +4,7 @@ import androidx.hilt.lifecycle.ViewModelInject
import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
@Deprecated("use etebase")
class CreateUserInfoViewModel @ViewModelInject constructor(
private val client: EteSyncClient): CompletableViewModel<String>() {
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {

@ -5,6 +5,7 @@ import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.ui.ActionViewModel
@Deprecated("use etebase")
class DeleteCalendarViewModel @ViewModelInject constructor(
private val client: EteSyncClient) : ActionViewModel() {
suspend fun deleteCalendar(account: CaldavAccount, calendar: CaldavCalendar) {

@ -31,6 +31,7 @@ import org.tasks.ui.DisplayableException
import java.net.ConnectException
import javax.inject.Inject
@Deprecated("use etebase")
@AndroidEntryPoint
class EncryptionSettingsActivity : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var encryption: KeyStoreEncryption

@ -25,6 +25,7 @@ import org.tasks.data.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
@Deprecated("use etebase")
@AndroidEntryPoint
class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var eteSyncClient: EteSyncClient
@ -53,9 +54,6 @@ class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
updateAccountViewModel.removeObserver(this)
}
override val description: Int
get() = R.string.etesync_account_description
private suspend fun addAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) {
caldavAccount = CaldavAccount()
caldavAccount!!.accountType = CaldavAccount.TYPE_ETESYNC

@ -7,6 +7,7 @@ import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
@Deprecated("use etebase")
@AndroidEntryPoint
class EteSyncCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
private val createCalendarViewModel: CreateCalendarViewModel by viewModels()

@ -38,6 +38,7 @@ import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
@Deprecated("use etebase")
class EteSyncClient {
private val encryption: KeyStoreEncryption
private val preferences: Preferences

@ -33,6 +33,7 @@ import java.util.*
import javax.inject.Inject
import kotlin.collections.HashSet
@Deprecated("use etebase")
class EteSynchronizer @Inject constructor(
@param:ApplicationContext private val context: Context,
private val caldavDao: CaldavDao,

@ -5,6 +5,7 @@ import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.ui.CompletableViewModel
@Deprecated("use etebase")
class UpdateCalendarViewModel @ViewModelInject constructor(
private val client: EteSyncClient): CompletableViewModel<String?>() {
suspend fun updateCalendar(account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) {

@ -6,6 +6,7 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.Strings.isNullOrEmpty
import org.tasks.ui.CompletableViewModel
@Deprecated("use etebase")
class UpdateEteSyncAccountViewModel @ViewModelInject constructor(
private val client: EteSyncClient) : CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun updateAccount(url: String, user: String, pass: String?, token: String) {

@ -0,0 +1,42 @@
package org.tasks.jobs
import android.content.Context
import androidx.hilt.Assisted
import androidx.hilt.work.WorkerInject
import androidx.work.WorkerParameters
import kotlinx.coroutines.*
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.data.CaldavAccount.Companion.TYPE_ETEBASE
import org.tasks.data.CaldavDao
import org.tasks.etebase.EteBaseSynchronizer
import org.tasks.preferences.Preferences
class SyncEteBaseWork @WorkerInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
firebase: Firebase,
localBroadcastManager: LocalBroadcastManager,
preferences: Preferences,
private val caldavDao: CaldavDao,
private val synchronizer: EteBaseSynchronizer
) : SyncWork(context, workerParams, firebase, localBroadcastManager, preferences) {
override suspend fun enabled() = caldavDao.getAccounts(TYPE_ETEBASE).isNotEmpty()
override val syncStatus = R.string.p_sync_ongoing_etebase
override suspend fun doSync() {
jobs().awaitAll()
}
private suspend fun jobs(): List<Deferred<Unit>> = coroutineScope {
caldavDao.getAccounts(TYPE_ETEBASE)
.map {
async(Dispatchers.IO) {
synchronizer.sync(it)
}
}
}
}

@ -13,6 +13,7 @@ import org.tasks.data.CaldavDao
import org.tasks.etesync.EteSynchronizer
import org.tasks.preferences.Preferences
@Deprecated("use etebase")
class SyncEteSyncWork @WorkerInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,

@ -20,8 +20,11 @@ interface WorkManager {
fun caldavSync(immediate: Boolean)
@Deprecated("use etebase")
fun eteSync(immediate: Boolean)
fun eteBaseSync(immediate: Boolean)
fun openTaskSync(immediate: Boolean)
fun reverseGeocode(place: Place)
@ -53,11 +56,13 @@ interface WorkManager {
const val TAG_MIDNIGHT_REFRESH = "tag_midnight_refresh"
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"
@Deprecated("use etebase") const val TAG_SYNC_ETESYNC = "tag_sync_etesync"
const val TAG_SYNC_ETEBASE = "tag_sync_etebase"
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"
@Deprecated("use etebase") const val TAG_BACKGROUND_SYNC_ETESYNC = "tag_background_sync_etesync"
const val TAG_BACKGROUND_SYNC_ETEBASE = "tag_background_sync_etebase"
const val TAG_BACKGROUND_SYNC_OPENTASKS = "tag_background_sync_opentasks"
const val TAG_REMOTE_CONFIG = "tag_remote_config"
const val TAG_MIGRATE_LOCAL = "tag_migrate_local"

@ -14,6 +14,7 @@ import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.data.*
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_ETEBASE
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
@ -24,6 +25,7 @@ import org.tasks.jobs.SyncWork.Companion.EXTRA_IMMEDIATE
import org.tasks.jobs.WorkManager.Companion.MAX_CLEANUP_LENGTH
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_ETEBASE
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
@ -33,6 +35,7 @@ import org.tasks.jobs.WorkManager.Companion.TAG_MIGRATE_LOCAL
import org.tasks.jobs.WorkManager.Companion.TAG_REFRESH
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_ETEBASE
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
@ -101,6 +104,9 @@ class WorkManagerImpl constructor(
override fun eteSync(immediate: Boolean) =
sync(immediate, TAG_SYNC_ETESYNC, SyncEteSyncWork::class.java)
override fun eteBaseSync(immediate: Boolean) =
sync(immediate, TAG_SYNC_ETEBASE, SyncEteBaseWork::class.java)
override fun openTaskSync(immediate: Boolean) =
sync(immediate, TAG_SYNC_OPENTASK, SyncOpenTasksWork::class.java, false)
@ -168,6 +174,13 @@ class WorkManagerImpl constructor(
enabled && caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty(),
unmetered)
}
throttle.run {
scheduleBackgroundSync(
TAG_BACKGROUND_SYNC_ETEBASE,
SyncEteBaseWork::class.java,
enabled && caldavDao.getAccounts(TYPE_ETEBASE).isNotEmpty(),
unmetered)
}
throttle.run {
scheduleBackgroundSync(
TAG_BACKGROUND_SYNC_OPENTASKS,

@ -25,9 +25,6 @@ class OpenTaskAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Too
}
}
override val description: Int
get() = 0
override val newPassword: String?
get() = ""

@ -24,6 +24,7 @@ import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_DAVx5
import org.tasks.data.OpenTaskDao.Companion.ACCOUNT_TYPE_ETESYNC
import org.tasks.etebase.EteBaseAccountSettingsActivity
import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.jobs.WorkManager
@ -149,7 +150,8 @@ class Synchronization : InjectingPreferenceFragment() {
if (isNullOrEmpty(error)) {
preference.setSummary(when {
account.isCaldavAccount -> R.string.caldav
account.isEteSyncAccount
account.isEteSyncAccount -> R.string.etesync_v1
account.isEteBaseAccount
|| (account.isOpenTasks
&& account.uuid?.startsWith(ACCOUNT_TYPE_ETESYNC) == true) ->
R.string.etesync
@ -165,6 +167,7 @@ class Synchronization : InjectingPreferenceFragment() {
val intent = Intent(context, when {
account.isCaldavAccount -> CaldavAccountSettingsActivity::class.java
account.isEteSyncAccount -> EteSyncAccountSettingsActivity::class.java
account.isEteBaseAccount -> EteBaseAccountSettingsActivity::class.java
account.isOpenTasks -> OpenTaskAccountSettingsActivity::class.java
else -> throw IllegalArgumentException("Unexpected account type: $account")
})

@ -17,7 +17,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.dialogs.DialogBuilder
import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.etebase.EteBaseAccountSettingsActivity
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_CALDAV_SETTINGS
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_GOOGLE_TASKS
import org.tasks.themes.DrawableUtil
@ -65,7 +65,7 @@ class AddAccountDialog : DialogFragment() {
Intent(activity, CaldavAccountSettingsActivity::class.java),
REQUEST_CALDAV_SETTINGS)
3 -> activity?.startActivityForResult(
Intent(activity, EteSyncAccountSettingsActivity::class.java),
Intent(activity, EteBaseAccountSettingsActivity::class.java),
REQUEST_CALDAV_SETTINGS)
}
dialog.dismiss()

@ -6,6 +6,7 @@ import kotlinx.coroutines.*
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_ETEBASE
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
@ -15,6 +16,7 @@ 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_ETEBASE
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
@ -36,7 +38,8 @@ class SyncAdapters @Inject constructor(
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) }
@Deprecated("use etebase") private val eteSync = Debouncer(TAG_SYNC_ETESYNC) { workManager.eteSync(it) }
private val eteBaseSync = Debouncer(TAG_SYNC_ETEBASE) { workManager.eteBaseSync(it) }
private val opentasks = Debouncer(TAG_SYNC_OPENTASK) { workManager.openTaskSync(it) }
private val syncStatus = Debouncer("sync_status") {
if (preferences.getBoolean(R.string.p_sync_ongoing_android, false) != it
@ -62,6 +65,9 @@ class SyncAdapters @Inject constructor(
if (caldavDao.isAccountType(task.id, TYPE_ETESYNC)) {
eteSync.sync(false)
}
if (caldavDao.isAccountType(task.id, TYPE_ETEBASE)) {
eteBaseSync.sync(false)
}
if (caldavDao.isAccountType(task.id, TYPE_OPENTASKS)) {
opentasks.sync(false)
}
@ -84,6 +90,7 @@ class SyncAdapters @Inject constructor(
val googleTasksEnabled = async { isGoogleTaskSyncEnabled() }
val caldavEnabled = async { isCaldavSyncEnabled() }
val eteSyncEnabled = async { isEteSyncEnabled() }
val eteBaseEnabled = async { isEteBaseEnabled() }
val opentasksEnabled = async { isOpenTaskSyncEnabled() }
if (googleTasksEnabled.await()) {
@ -98,6 +105,10 @@ class SyncAdapters @Inject constructor(
eteSync.sync(immediate)
}
if (eteBaseEnabled.await()) {
eteBaseSync.sync(immediate)
}
if (opentasksEnabled.await()) {
opentasks.sync(immediate)
}
@ -108,8 +119,11 @@ class SyncAdapters @Inject constructor(
private suspend fun isCaldavSyncEnabled() =
caldavDao.getAccounts(TYPE_CALDAV, TYPE_TASKS).isNotEmpty()
@Deprecated("use etebase")
private suspend fun isEteSyncEnabled() = caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty()
private suspend fun isEteBaseEnabled() = caldavDao.getAccounts(TYPE_ETEBASE).isNotEmpty()
private suspend fun isOpenTaskSyncEnabled() =
caldavDao.getAccounts(TYPE_OPENTASKS).isNotEmpty()
|| openTaskDao.newAccounts().isNotEmpty()

@ -12,6 +12,7 @@
<string name="FSA_label">Tasks Shortcut</string>
<string name="caldav">CalDAV</string>
<string name="etesync">EteSync</string>
<string name="etesync_v1">EteSync v1</string>
<string name="davx5">DAVx⁵</string>
<string name="tasks_org">Tasks.org</string>
<string name="etesync_url">https://api.etesync.com</string>
@ -390,6 +391,7 @@
<string name="p_sync_ongoing_google_tasks">sync_ongoing_google_tasks</string>
<string name="p_sync_ongoing_caldav">sync_ongoing_caldav</string>
<string name="p_sync_ongoing_etesync">sync_ongoing_etesync</string>
<string name="p_sync_ongoing_etebase">sync_ongoing_etebase</string>
<string name="p_sync_ongoing_opentasks">sync_ongoing_opentasks</string>
<string name="p_sync_ongoing_android">sync_ongoing_android</string>
<string name="p_last_backup">last_backup</string>

Loading…
Cancel
Save