mirror of https://github.com/tasks/tasks
EteSync v2 support
parent
9470eb2786
commit
b55a783138
@ -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,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue