mirror of https://github.com/tasks/tasks
EteSync v2 support
parent
9470eb2786
commit
b55a783138
@ -1,23 +1,15 @@
|
||||
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 clientProvider: EteBaseClientProvider): CompletableViewModel<Pair<UserInfo, String>>() {
|
||||
private val clientProvider: EteBaseClientProvider): CompletableViewModel<String>() {
|
||||
suspend fun addAccount(url: String, username: String, password: String) {
|
||||
run {
|
||||
val token =
|
||||
clientProvider
|
||||
.forUrl(url, username, null, null)
|
||||
.setForeground()
|
||||
.getToken(password)
|
||||
Pair.create(
|
||||
clientProvider.forUrl(url, username, null, token!!).userInfo(),
|
||||
token
|
||||
)
|
||||
clientProvider
|
||||
.forUrl(url, username, password, foreground = true)
|
||||
.getSession()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
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 android.content.Context
|
||||
import com.etebase.client.*
|
||||
import com.etebase.client.Collection
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
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.google.common.collect.Lists
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
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 java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class EteBaseClient(
|
||||
private val customCertManager: CustomCertManager,
|
||||
private val username: String?,
|
||||
private val encryptionPassword: String?,
|
||||
private val token: String?,
|
||||
private val httpClient: OkHttpClient,
|
||||
private val httpUrl: HttpUrl
|
||||
private val context: Context,
|
||||
private val username: String,
|
||||
private val etebase: Account,
|
||||
private val caldavDao: CaldavDao
|
||||
) {
|
||||
private val journalManager = JournalManager(httpClient, httpUrl)
|
||||
private val cache = EtebaseLocalCache.getInstance(context, username)
|
||||
|
||||
@Throws(IOException::class, Exceptions.HttpException::class)
|
||||
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
|
||||
JournalAuthenticator(httpClient, httpUrl).getAuthToken(username!!, password!!)
|
||||
}
|
||||
fun getSession(): String = etebase.save(null)
|
||||
|
||||
@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!!)
|
||||
suspend fun getCollections(): List<Collection> {
|
||||
val collectionManager = etebase.collectionManager
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
collectionManager.list(TYPE_TASKS)
|
||||
}
|
||||
if (userInfo == null) {
|
||||
throw RuntimeException("Missing userInfo")
|
||||
response.data.forEach {
|
||||
cache.collectionSet(collectionManager, it)
|
||||
}
|
||||
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
|
||||
response.removedMemberships.forEach {
|
||||
cache.collectionUnset(collectionManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
return cache.collectionList(collectionManager)
|
||||
}
|
||||
|
||||
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
|
||||
suspend fun getSyncEntries(
|
||||
userInfo: UserInfoManager.UserInfo?,
|
||||
journal: Journal,
|
||||
suspend fun fetchItems(
|
||||
collection: Collection,
|
||||
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>
|
||||
callback: suspend (Pair<String?, List<Item>>) -> Unit
|
||||
) {
|
||||
val itemManager = etebase.collectionManager.getItemManager(collection)
|
||||
var stoken = calendar.ctag
|
||||
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)
|
||||
val items = withContext(Dispatchers.IO) {
|
||||
itemManager.list(FetchOptions().stoken(stoken).limit(MAX_FETCH))
|
||||
}
|
||||
stoken = items.stoken
|
||||
callback(Pair(stoken, items.data.toList()))
|
||||
} while (!items.isDone)
|
||||
}
|
||||
|
||||
@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
|
||||
suspend fun updateItem(collection: Collection, task: CaldavTask, content: ByteArray): Item {
|
||||
val itemManager = etebase.collectionManager.getItemManager(collection)
|
||||
val item = cache.itemGet(itemManager, collection.uid, task.`object`!!)
|
||||
?: itemManager
|
||||
.create(ItemMetadata().apply { name = task.remoteId!! }, "")
|
||||
.apply {
|
||||
task.`object` = uid
|
||||
caldavDao.update(task)
|
||||
}
|
||||
item.meta = item.meta.let { meta ->
|
||||
meta.mtime = currentTimeMillis()
|
||||
meta
|
||||
}
|
||||
item.content = content
|
||||
return item
|
||||
}
|
||||
|
||||
fun setForeground(): EteBaseClient {
|
||||
customCertManager.appInForeground = true
|
||||
return this
|
||||
suspend fun deleteItem(collection: Collection, uid: String): Item? {
|
||||
val itemManager = etebase.collectionManager.getItemManager(collection)
|
||||
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 {
|
||||
JournalAuthenticator(httpClient, httpUrl).invalidateAuthToken(token!!)
|
||||
EtebaseLocalCache.clear(context, username)
|
||||
withContext(Dispatchers.IO) {
|
||||
etebase.logout()
|
||||
}
|
||||
} 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
|
||||
}
|
||||
suspend fun makeCollection(name: String, color: Int) =
|
||||
etebase
|
||||
.collectionManager
|
||||
.create(TYPE_TASKS, ItemMetadata(), "")
|
||||
.let { setAndUpload(it, name, color) }
|
||||
|
||||
@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
|
||||
}
|
||||
suspend fun updateCollection(calendar: CaldavCalendar, name: String, color: Int) =
|
||||
cache
|
||||
.collectionGet(etebase.collectionManager, calendar.url!!)
|
||||
.let { setAndUpload(it, name, color) }
|
||||
|
||||
@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)
|
||||
suspend fun deleteCollection(calendar: CaldavCalendar) =
|
||||
cache
|
||||
.collectionGet(etebase.collectionManager, calendar.url!!)
|
||||
.apply { delete() }
|
||||
.let { setAndUpload(it) }
|
||||
|
||||
private suspend fun setAndUpload(
|
||||
collection: Collection,
|
||||
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 {
|
||||
private const val TYPE_TASKS = "TASKS"
|
||||
private const val MAX_FETCH = 50
|
||||
private const val MAX_PUSH = 30
|
||||
private const val TYPE_TASKS = "etebase.vtodo"
|
||||
private const val MAX_FETCH = 30L
|
||||
|
||||
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
|
||||
|
||||
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 clientProvider: EteBaseClientProvider) : CompletableViewModel<Pair<UserInfo, String>>() {
|
||||
suspend fun updateAccount(url: String, user: String, pass: String?, token: String) {
|
||||
private val clientProvider: EteBaseClientProvider) : CompletableViewModel<String>() {
|
||||
suspend fun updateAccount(url: String, user: String, pass: String?, session: String) {
|
||||
run {
|
||||
if (isNullOrEmpty(pass)) {
|
||||
Pair.create(
|
||||
clientProvider.forUrl(url, user, null, token).setForeground().userInfo(),
|
||||
token
|
||||
)
|
||||
clientProvider.forUrl(url, user, null, session, true).getSession()
|
||||
} else {
|
||||
val newToken =
|
||||
clientProvider
|
||||
.forUrl(url, user, null, null)
|
||||
.setForeground()
|
||||
.getToken(pass)!!
|
||||
Pair.create(
|
||||
clientProvider.forUrl(url, user, null, newToken).userInfo(),
|
||||
newToken
|
||||
)
|
||||
clientProvider
|
||||
.forUrl(url, user, pass, foreground = true)
|
||||
.getSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue