EteSync v2 support

pull/1244/head
Alex Baker 4 years ago
parent 9470eb2786
commit b55a783138

@ -609,10 +609,6 @@
android:name=".etebase.EteBaseCalendarSettingsActivity" android:name=".etebase.EteBaseCalendarSettingsActivity"
android:theme="@style/Tasks" /> android:theme="@style/Tasks" />
<activity
android:name=".etebase.EncryptionSettingsActivity"
android:theme="@style/Tasks" />
<activity <activity
android:name=".preferences.MainPreferences" android:name=".preferences.MainPreferences"
android:theme="@style/Tasks" /> android:theme="@style/Tasks" />

@ -80,7 +80,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
get() = caldavCalendar == null get() = caldavCalendar == null
override val toolbarTitle: String override val toolbarTitle: String
get() = if (isNew) getString(R.string.new_list) else caldavCalendar!!.name!! get() = if (isNew) getString(R.string.new_list) else caldavCalendar!!.name ?: ""
@OnTextChanged(R.id.name) @OnTextChanged(R.id.name)
fun onNameChanged() { fun onNameChanged() {

@ -47,6 +47,7 @@ class CaldavAccount : Parcelable {
@ColumnInfo(name = "cda_repeat") @ColumnInfo(name = "cda_repeat")
var isSuppressRepeatingTasks = false var isSuppressRepeatingTasks = false
@Deprecated("use etebase")
@ColumnInfo(name = "cda_encryption_key") @ColumnInfo(name = "cda_encryption_key")
@Transient @Transient
var encryptionKey: String? = null var encryptionKey: String? = null
@ -78,6 +79,7 @@ class CaldavAccount : Parcelable {
return encryption.decrypt(password) ?: "" return encryption.decrypt(password) ?: ""
} }
@Deprecated("use etebase")
fun getEncryptionPassword(encryption: KeyStoreEncryption): String { fun getEncryptionPassword(encryption: KeyStoreEncryption): String {
return encryption.decrypt(encryptionKey) ?: "" return encryption.decrypt(encryptionKey) ?: ""
} }

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

@ -1,15 +0,0 @@
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 clientProvider: EteBaseClientProvider): CompletableViewModel<String>() {
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {
run {
clientProvider.forAccount(caldavAccount).createUserInfo(derivedKey)
derivedKey
}
}
}

@ -1,188 +0,0 @@
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"
}
}

@ -1,28 +1,19 @@
package org.tasks.etebase package org.tasks.etebase
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.util.Pair
import androidx.lifecycle.lifecycleScope
import butterknife.OnCheckedChanged 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.data.Task
import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Constants import org.tasks.analytics.Constants
import org.tasks.caldav.BaseCaldavAccountSettingsActivity import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -56,52 +47,27 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
override val description: Int override val description: Int
get() = R.string.etesync_account_description get() = R.string.etesync_account_description
private suspend fun addAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) { private suspend fun addAccount(session: String) {
caldavAccount = CaldavAccount() caldavAccount = CaldavAccount()
caldavAccount!!.accountType = CaldavAccount.TYPE_ETEBASE caldavAccount!!.accountType = CaldavAccount.TYPE_ETEBASE
caldavAccount!!.uuid = UUIDHelper.newUUID() caldavAccount!!.uuid = UUIDHelper.newUUID()
applyTo(caldavAccount!!, userInfoAndToken) applyTo(caldavAccount!!, session)
} }
private suspend fun updateAccount(userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) { private suspend fun updateAccount(session: String) {
caldavAccount!!.error = "" caldavAccount!!.error = ""
applyTo(caldavAccount!!, userInfoAndToken) applyTo(caldavAccount!!, session)
} }
private suspend fun applyTo(account: CaldavAccount, userInfoAndToken: Pair<UserInfoManager.UserInfo, String>) { private suspend fun applyTo(account: CaldavAccount, session: String) {
hideProgressIndicator() hideProgressIndicator()
account.name = newName account.name = newName
account.url = newURL account.url = newURL
account.username = newUsername account.username = newUsername
val token = userInfoAndToken.second if (session != account.getPassword(encryption)) {
if (token != account.getPassword(encryption)) { account.password = encryption.encrypt(session)
account.password = encryption.encrypt(token!!)
} }
val userInfo = userInfoAndToken.first saveAccountAndFinish()
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) @OnCheckedChanged(R.id.show_advanced)
@ -133,10 +99,10 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
} }
override val newURL: String override val newURL: String
get() { get() =
val url = super.newURL super.newURL
return if (isNullOrEmpty(url)) getString(R.string.etesync_url) else url // TODO: change to etebase url .takeIf { it.isNotBlank() }
} ?: getString(R.string.etebase_url)
override val newPassword: String override val newPassword: String
get() = binding.password.text.toString().trim { it <= ' ' } get() = binding.password.text.toString().trim { it <= ' ' }
@ -144,20 +110,6 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
override val helpUrl: String override val helpUrl: String
get() = getString(R.string.url_etesync) 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() { private suspend fun saveAccountAndFinish() {
if (caldavAccount!!.id == Task.NO_ID) { if (caldavAccount!!.id == Task.NO_ID) {
caldavDao.insert(caldavAccount!!) caldavDao.insert(caldavAccount!!)
@ -173,11 +125,7 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
} }
override suspend fun removeAccount() { override suspend fun removeAccount() {
caldavAccount?.let { clientProvider.forAccount(it).invalidateToken() } caldavAccount?.let { clientProvider.forAccount(it).logout() }
super.removeAccount() super.removeAccount()
} }
companion object {
private const val REQUEST_ENCRYPTION_PASSWORD = 10101
}
} }

@ -1,177 +1,159 @@
package org.tasks.etebase package org.tasks.etebase
import androidx.core.util.Pair import android.content.Context
import at.bitfire.cert4android.CustomCertManager import com.etebase.client.*
import com.etesync.journalmanager.* import com.etebase.client.Collection
import com.etesync.journalmanager.Constants.Companion.CURRENT_VERSION import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.Crypto.AsymmetricKeyPair
import com.etesync.journalmanager.Crypto.CryptoManager
import com.etesync.journalmanager.Exceptions.IntegrityException import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException 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.google.common.collect.Lists
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.util.*
class EteBaseClient( class EteBaseClient(
private val customCertManager: CustomCertManager, private val context: Context,
private val username: String?, private val username: String,
private val encryptionPassword: String?, private val etebase: Account,
private val token: String?, private val caldavDao: CaldavDao
private val httpClient: OkHttpClient,
private val httpUrl: HttpUrl
) { ) {
private val journalManager = JournalManager(httpClient, httpUrl) private val cache = EtebaseLocalCache.getInstance(context, username)
@Throws(IOException::class, Exceptions.HttpException::class) @Throws(IOException::class, Exceptions.HttpException::class)
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) { fun getSession(): String = etebase.save(null)
JournalAuthenticator(httpClient, httpUrl).getAuthToken(username!!, password!!)
}
@Throws(Exceptions.HttpException::class) @Throws(Exceptions.HttpException::class)
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) { suspend fun getCollections(): List<Collection> {
val userInfoManager = UserInfoManager(httpClient, httpUrl) val collectionManager = etebase.collectionManager
userInfoManager.fetch(username!!) val response = withContext(Dispatchers.IO) {
} collectionManager.list(TYPE_TASKS)
@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) { response.data.forEach {
throw RuntimeException("Missing userInfo") cache.collectionSet(collectionManager, it)
} }
val cryptoManager = CryptoManager(userInfo.version!!.toInt(), encryptionPassword!!, "userInfo") response.removedMemberships.forEach {
val keyPair = AsymmetricKeyPair(userInfo.getContent(cryptoManager)!!, userInfo.pubkey!!) cache.collectionUnset(collectionManager, it)
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
} }
} return cache.collectionList(collectionManager)
@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) @Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
suspend fun getSyncEntries( suspend fun fetchItems(
userInfo: UserInfoManager.UserInfo?, collection: Collection,
journal: Journal,
calendar: CaldavCalendar, calendar: CaldavCalendar,
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) { callback: suspend (Pair<String?, List<Item>>) -> Unit
val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!) ) {
val crypto = getCrypto(userInfo, journal) val itemManager = etebase.collectionManager.getItemManager(collection)
var journalEntries: List<JournalEntryManager.Entry> var stoken = calendar.ctag
do { do {
journalEntries = journalEntryManager.list(crypto, calendar.ctag, MAX_FETCH) val items = withContext(Dispatchers.IO) {
callback.invoke(journalEntries.map { itemManager.list(FetchOptions().stoken(stoken).limit(MAX_FETCH))
Pair.create(it, SyncEntry.fromJournalEntry(crypto, it)) }
}) stoken = items.stoken
} while (journalEntries.size >= MAX_FETCH) callback(Pair(stoken, items.data.toList()))
} while (!items.isDone)
} }
@Throws(Exceptions.HttpException::class) suspend fun updateItem(collection: Collection, task: CaldavTask, content: ByteArray): Item {
suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) { val itemManager = etebase.collectionManager.getItemManager(collection)
var remoteCtag = remoteCtag val item = cache.itemGet(itemManager, collection.uid, task.`object`!!)
val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!) ?: itemManager
for (partition in Lists.partition(entries!!, MAX_PUSH)) { .create(ItemMetadata().apply { name = task.remoteId!! }, "")
journalEntryManager.create(partition, remoteCtag) .apply {
remoteCtag = partition[partition.size - 1].uid task.`object` = uid
caldavDao.update(task)
}
item.meta = item.meta.let { meta ->
meta.mtime = currentTimeMillis()
meta
} }
item.content = content
return item
} }
fun setForeground(): EteBaseClient { suspend fun deleteItem(collection: Collection, uid: String): Item? {
customCertManager.appInForeground = true val itemManager = etebase.collectionManager.getItemManager(collection)
return this return cache.itemGet(itemManager, collection.uid, uid)?.apply { delete() }
} }
suspend fun invalidateToken() = withContext(Dispatchers.IO) { suspend fun updateCache(collection: Collection, items: List<Item>) {
val itemManager = etebase.collectionManager.getItemManager(collection)
items.forEach { cache.itemSet(itemManager, collection.uid, it) }
}
suspend fun uploadChanges(collection: Collection, items: List<Item>) {
val itemManager = etebase.collectionManager.getItemManager(collection)
withContext(Dispatchers.IO) {
itemManager.batch(items.toTypedArray())
}
}
suspend fun getItem(collection: Collection, uid: String): Item? =
cache.itemGet(
etebase.collectionManager.getItemManager(collection),
collection.uid,
uid
)
suspend fun logout() {
try { try {
JournalAuthenticator(httpClient, httpUrl).invalidateAuthToken(token!!) EtebaseLocalCache.clear(context, username)
withContext(Dispatchers.IO) {
etebase.logout()
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
} }
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class) @Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
suspend fun makeCollection(name: String?, color: Int): String = withContext(Dispatchers.IO) { suspend fun makeCollection(name: String, color: Int) =
val uid = Journal.genUid() etebase
val collectionInfo = CollectionInfo() .collectionManager
collectionInfo.displayName = name .create(TYPE_TASKS, ItemMetadata(), "")
collectionInfo.type = TYPE_TASKS .let { setAndUpload(it, name, color) }
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) @Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String = withContext(Dispatchers.IO) { suspend fun updateCollection(calendar: CaldavCalendar, name: String, color: Int) =
val uid = calendar.url cache
val journal = journalManager.fetch(uid!!) .collectionGet(etebase.collectionManager, calendar.url!!)
val userInfo = userInfo() .let { setAndUpload(it, name, color) }
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) @Throws(Exceptions.HttpException::class)
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) { suspend fun deleteCollection(calendar: CaldavCalendar) =
journalManager.delete(Journal.fakeWithUid(calendar.url!!)) cache
} .collectionGet(etebase.collectionManager, calendar.url!!)
.apply { delete() }
@Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class) .let { setAndUpload(it) }
suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) {
val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo") private suspend fun setAndUpload(
val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!) collection: Collection,
UserInfoManager(httpClient, httpUrl).create(userInfo) name: String? = null,
color: Int? = null
): String {
collection.meta = collection.meta.let { meta ->
name?.let { meta.name = it }
color?.let { meta.color = it.toHexColor() }
meta
}
val collectionManager = etebase.collectionManager
withContext(Dispatchers.IO) {
collectionManager.upload(collection)
}
cache.collectionSet(collectionManager, collection)
return collection.uid
} }
companion object { companion object {
private const val TYPE_TASKS = "TASKS" private const val TYPE_TASKS = "etebase.vtodo"
private const val MAX_FETCH = 50 private const val MAX_FETCH = 30L
private const val MAX_PUSH = 30
private fun Int.toHexColor(): String? = takeIf { this != 0 }?.let {
java.lang.String.format("#%06X", 0xFFFFFF and it)
}
} }
} }

@ -1,21 +1,28 @@
package org.tasks.etebase package org.tasks.etebase
import android.content.Context import android.content.Context
import android.os.Build
import at.bitfire.cert4android.CustomCertManager import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.util.TokenAuthenticator import com.etebase.client.Account
import com.etebase.client.Client
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.BuildConfig
import org.tasks.DebugNetworkInterceptor import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.MemoryCookieStore import org.tasks.caldav.MemoryCookieStore
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption import org.tasks.security.KeyStoreEncryption
import java.io.IOException
import java.security.KeyManagementException import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -24,46 +31,38 @@ class EteBaseClientProvider @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption, private val encryption: KeyStoreEncryption,
private val preferences: Preferences, private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor private val interceptor: DebugNetworkInterceptor,
private val caldavDao: CaldavDao
) { ) {
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class) @Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient { suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl( return forUrl(
account.url!!, account.url!!,
account.username, account.username!!,
account.getEncryptionPassword(encryption), null,
account.getPassword(encryption)) account.getPassword(encryption))
} }
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class) @Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) { suspend fun forUrl(url: String, username: String, password: String?, session: String? = null, foreground: Boolean = false): EteBaseClient = withContext(Dispatchers.IO) {
val customCertManager = newCertManager() val httpClient = createHttpClient(foreground)
EteBaseClient( val client = Client.create(httpClient, url)
customCertManager, val etebase = session
username, ?.let { Account.restore(client, it, null) }
encryptionPassword, ?: Account.login(client, username, password!!)
token, EteBaseClient(context, username, etebase, caldavDao)
createHttpClient(token, customCertManager),
url.toHttpUrl()
)
} }
private suspend fun newCertManager() = withContext(Dispatchers.Default) { private suspend fun createHttpClient(foreground: Boolean): OkHttpClient {
CustomCertManager(context) val customCertManager = withContext(Dispatchers.Default) {
} CustomCertManager(context, foreground)
}
private fun createHttpClient(
token: String?,
customCertManager: CustomCertManager,
foreground: Boolean = false
): OkHttpClient {
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier) val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS") val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(customCertManager), null) sslContext.init(null, arrayOf(customCertManager), null)
val builder = OkHttpClient() val builder = OkHttpClient()
.newBuilder() .newBuilder()
.addNetworkInterceptor(TokenAuthenticator(null, token)) .addNetworkInterceptor(UserAgentInterceptor)
.cookieJar(MemoryCookieStore()) .cookieJar(MemoryCookieStore())
.followRedirects(false) .followRedirects(false)
.followSslRedirects(true) .followSslRedirects(true)
@ -77,4 +76,18 @@ class EteBaseClientProvider @Inject constructor(
} }
return builder.build() return builder.build()
} }
private object UserAgentInterceptor : Interceptor {
private val userAgent = "${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME} (okhttp3) Android/${Build.VERSION.RELEASE}"
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val locale = Locale.getDefault()
val request = chain.request().newBuilder()
.header("User-Agent", userAgent)
.header("Accept-Language", locale.language + "-" + locale.country + ", " + locale.language + ";q=0.7, *;q=0.5")
.build()
return chain.proceed(request)
}
}
} }

@ -1,16 +1,13 @@
package org.tasks.etebase package org.tasks.etebase
import android.content.Context import android.content.Context
import androidx.core.util.Pair import android.graphics.Color
import at.bitfire.ical4android.ICalendar.Companion.prodId import at.bitfire.ical4android.ICalendar.Companion.prodId
import com.etebase.client.Collection
import com.etebase.client.Item
import com.etesync.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.journalmanager.Exceptions.IntegrityException import com.etesync.journalmanager.Exceptions.IntegrityException
import com.etesync.journalmanager.Exceptions.VersionTooNewException 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.helper.UUIDHelper
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -31,7 +28,6 @@ import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.HashSet
class EteBaseSynchronizer @Inject constructor( class EteBaseSynchronizer @Inject constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
@ -58,10 +54,6 @@ class EteBaseSynchronizer @Inject constructor(
setError(account, context.getString(R.string.password_required)) setError(account, context.getString(R.string.password_required))
return return
} }
if (isNullOrEmpty(account.encryptionKey)) {
setError(account, context.getString(R.string.encryption_password_required))
return
}
try { try {
synchronize(account) synchronize(account)
} catch (e: KeyManagementException) { } catch (e: KeyManagementException) {
@ -80,36 +72,34 @@ class EteBaseSynchronizer @Inject constructor(
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class) @Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
private suspend fun synchronize(account: CaldavAccount) { private suspend fun synchronize(account: CaldavAccount) {
val client = clientProvider.forAccount(account) val client = clientProvider.forAccount(account)
val userInfo = client.userInfo() val collections = client.getCollections()
val resources = client.getCalendars(userInfo) val uids = collections.map { it.uid }
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()
Timber.d("Found uids: %s", uids) Timber.d("Found uids: %s", uids)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, uids.toList())) { for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, uids)) {
taskDeleter.delete(calendar) taskDeleter.delete(calendar)
} }
for ((key, collection) in resources) { for (collection in collections) {
val uid = collection.uid val uid = collection.uid
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, uid!!) var calendar = caldavDao.getCalendarByUrl(account.uuid!!, uid)
val colorInt = collection.color val meta = collection.meta
val color = colorInt ?: 0 val color = meta.color?.let { Color.parseColor(it) } ?: 0
if (calendar == null) { if (calendar == null) {
calendar = CaldavCalendar() calendar = CaldavCalendar()
calendar.name = collection.displayName calendar.name = meta.name
calendar.account = account.uuid calendar.account = account.uuid
calendar.url = collection.uid calendar.url = collection.uid
calendar.uuid = UUIDHelper.newUUID() calendar.uuid = UUIDHelper.newUUID()
calendar.color = color calendar.color = color
caldavDao.insert(calendar) caldavDao.insert(calendar)
} else { } else {
if (calendar.name != collection.displayName if (calendar.name != meta.name || calendar.color != color) {
|| calendar.color != color) { calendar.name = meta.name
calendar.name = collection.displayName
calendar.color = color calendar.color = color
caldavDao.update(calendar) caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
} }
sync(client, userInfo!!, calendar, key) sync(client, calendar, collection)
} }
setError(account, "") setError(account, "")
} }
@ -126,59 +116,47 @@ class EteBaseSynchronizer @Inject constructor(
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class) @Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
private suspend fun sync( private suspend fun sync(
client: EteBaseClient, client: EteBaseClient,
userInfo: UserInfoManager.UserInfo,
caldavCalendar: CaldavCalendar, caldavCalendar: CaldavCalendar,
journal: Journal) { collection: Collection
) {
Timber.d("sync(%s)", caldavCalendar) Timber.d("sync(%s)", caldavCalendar)
val localChanges = HashMap<String?, CaldavTaskContainer>() val localChanges = HashMap<String?, CaldavTaskContainer>()
for (task in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) { for (task in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) {
localChanges[task.remoteId] = task localChanges[task.remoteId] = task
} }
var remoteCtag = journal.lastUid val remoteCtag = collection.stoken
if (isNullOrEmpty(remoteCtag) || remoteCtag != caldavCalendar.ctag) { if (isNullOrEmpty(remoteCtag) || remoteCtag != caldavCalendar.ctag) {
Timber.v("Applying remote changes") Timber.d("${caldavCalendar.name}: Applying remote changes")
client.getSyncEntries(userInfo, journal, caldavCalendar) { client.fetchItems(collection, caldavCalendar) {
applyEntries(caldavCalendar, it, localChanges.keys) applyEntries(caldavCalendar, it, localChanges.keys)
client.updateCache(collection, it.second)
} }
} else { } else {
Timber.d("%s up to date", caldavCalendar.name) Timber.d("${caldavCalendar.name} up to date")
} }
val changes: MutableList<SyncEntry> = ArrayList() val changes = ArrayList<Item>()
for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) { for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) {
val vtodo = task.vtodo client.deleteItem(collection, task.remoteId!!)?.let { changes.add(it) }
if (!isNullOrEmpty(vtodo)) {
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE))
}
} }
for (task in localChanges.values) { for (task in localChanges.values) {
val vtodo = task.vtodo val vtodo = task.vtodo
val existingTask = !isNullOrEmpty(vtodo) val existingTask = !isNullOrEmpty(vtodo)
if (task.isDeleted) { if (task.isDeleted) {
if (existingTask) { if (existingTask) {
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE)) client.deleteItem(collection, task.remoteId!!)?.let { changes.add(it) }
} }
} else { } else {
changes.add( changes.add(client.updateItem(
SyncEntry( collection,
String(iCal.toVtodo(task.caldavTask, task.task)), task.caldavTask,
if (existingTask) SyncEntry.Actions.CHANGE else SyncEntry.Actions.ADD)) iCal.toVtodo(task.caldavTask, task.task)
))
} }
} }
remoteCtag = caldavCalendar.ctag if (changes.isNotEmpty()) {
val crypto = client.getCrypto(userInfo, journal) client.uploadChanges(collection, changes)
val updates: MutableList<Pair<JournalEntryManager.Entry, SyncEntry>> = ArrayList() applyEntries(caldavCalendar, Pair(caldavCalendar.ctag, changes), HashSet())
var previous: JournalEntryManager.Entry? = if (isNullOrEmpty(remoteCtag)) null else getFakeWithUid(remoteCtag!!) client.updateCache(collection, changes)
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) Timber.d("UPDATE %s", caldavCalendar)
caldavDao.update(caldavCalendar) caldavDao.update(caldavCalendar)
@ -188,37 +166,34 @@ class EteBaseSynchronizer @Inject constructor(
private suspend fun applyEntries( private suspend fun applyEntries(
caldavCalendar: CaldavCalendar, caldavCalendar: CaldavCalendar,
syncEntries: List<Pair<JournalEntryManager.Entry, SyncEntry>>, items: Pair<String?, List<Item>>,
dirty: MutableSet<String?>) { dirty: MutableSet<String?>) {
for (entry in syncEntries) { for (item in items.second) {
val journalEntry = entry.first val vtodo = item.contentString
val syncEntry = entry.second
val action = syncEntry!!.action
val vtodo = syncEntry.content
Timber.v("%s: %s", action, vtodo)
val task = fromVtodo(vtodo) ?: continue val task = fromVtodo(vtodo) ?: continue
val remoteId = task.uid val remoteId = task.uid
val caldavTask = caldavDao.getTaskByRemoteId(caldavCalendar.uuid!!, remoteId!!) val caldavTask = caldavDao.getTaskByRemoteId(caldavCalendar.uuid!!, remoteId!!)
when (action) { if (item.isDeleted) {
SyncEntry.Actions.ADD, SyncEntry.Actions.CHANGE -> if (dirty.contains(remoteId)) { dirty.remove(remoteId)
if (caldavTask != null) {
if (caldavTask.isDeleted()) {
caldavDao.delete(caldavTask)
} else {
taskDeleter.delete(caldavTask.task)
}
}
} else {
caldavTask?.`object` = item.uid
if (dirty.contains(remoteId)) {
caldavTask!!.vtodo = vtodo caldavTask!!.vtodo = vtodo
caldavDao.update(caldavTask) caldavDao.update(caldavTask)
} else { } else {
iCal.fromVtodo(caldavCalendar, caldavTask, task, vtodo, null, null) iCal.fromVtodo(caldavCalendar, caldavTask, task, vtodo, item.uid, 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)
} }
caldavCalendar.ctag = items.first
Timber.d("Setting stoken to ${caldavCalendar.ctag}")
caldavDao.update(caldavCalendar)
} }
} }

@ -0,0 +1,105 @@
package org.tasks.etebase
import android.content.Context
import com.etebase.client.*
import com.etebase.client.Collection
import com.etebase.client.exceptions.EtebaseException
import com.etebase.client.exceptions.UrlParseException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.*
class EtebaseLocalCache private constructor(context: Context, username: String) {
private val fsCache: FileSystemCache = FileSystemCache.create(context.filesDir.absolutePath, username)
private suspend fun clearUserCache() {
withContext(Dispatchers.IO) {
fsCache.clearUserCache()
}
}
suspend fun collectionList(colMgr: CollectionManager): List<Collection> =
withContext(Dispatchers.IO) {
fsCache._unstable_collectionList(colMgr).filter { !it.isDeleted }
}
suspend fun collectionGet(colMgr: CollectionManager, colUid: String): Collection =
withContext(Dispatchers.IO) {
fsCache.collectionGet(colMgr, colUid)
}
suspend fun collectionSet(colMgr: CollectionManager, collection: Collection) {
if (collection.isDeleted) {
collectionUnset(colMgr, collection.uid)
} else {
withContext(Dispatchers.IO) {
fsCache.collectionSet(colMgr, collection)
}
}
}
suspend fun collectionUnset(colMgr: CollectionManager, collection: RemovedCollection) {
collectionUnset(colMgr, collection.uid())
}
private suspend fun collectionUnset(colMgr: CollectionManager, colUid: String) {
withContext(Dispatchers.IO) {
try {
fsCache.collectionUnset(colMgr, colUid)
} catch (e: UrlParseException) {
// Ignore, as it just means the file doesn't exist
}
}
}
suspend fun itemGet(itemMgr: ItemManager, colUid: String, itemUid: String): Item? =
withContext(Dispatchers.IO) {
// Need the try because the inner call doesn't return null on missing, but an error
try {
fsCache.itemGet(itemMgr, colUid, itemUid)
} catch (e: EtebaseException) {
null
}
}
suspend fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) {
withContext(Dispatchers.IO) {
if (item.isDeleted) {
fsCache.itemUnset(itemMgr, colUid, item.uid)
} else {
fsCache.itemSet(itemMgr, colUid, item)
}
}
}
companion object {
private val localCacheCache: HashMap<String, EtebaseLocalCache> = HashMap()
fun getInstance(context: Context, username: String): EtebaseLocalCache {
synchronized(localCacheCache) {
val cached = localCacheCache[username]
return if (cached != null) {
cached
} else {
val ret = EtebaseLocalCache(context, username)
localCacheCache[username] = ret
ret
}
}
}
fun clear(context: Context) = runBlocking {
val users = synchronized(localCacheCache) {
localCacheCache.keys.toList()
}
users.forEach { clear(context, it) }
}
suspend fun clear(context: Context, username: String) {
val localCache = getInstance(context, username)
localCache.clearUserCache()
localCacheCache.remove(username)
}
}
}

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

@ -15,6 +15,7 @@ import org.tasks.PermissionUtil
import org.tasks.R import org.tasks.R
import org.tasks.calendars.CalendarEventProvider import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.etebase.EtebaseLocalCache
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.FragmentPermissionRequestor import org.tasks.preferences.FragmentPermissionRequestor
@ -205,8 +206,9 @@ class Advanced : InjectingPreferenceFragment() {
.newDialog() .newDialog()
.setMessage(R.string.EPr_delete_task_data_warning) .setMessage(R.string.EPr_delete_task_data_warning)
.setPositiveButton(R.string.EPr_delete_task_data) { _, _ -> .setPositiveButton(R.string.EPr_delete_task_data) { _, _ ->
context?.deleteDatabase(database.name) val context = requireContext()
requireContext().deleteDatabase(database.name) context.deleteDatabase(database.name)
EtebaseLocalCache.clear(context)
restart() restart()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)

Loading…
Cancel
Save