mirror of https://github.com/tasks/tasks
Copy etesync package to etebase, deprecate etesync
parent
f4e63d6b59
commit
af2213d60f
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue