mirror of https://github.com/tasks/tasks
Remove native EteSync v1 support
EteSync v1 accounts can still be used with the EteSync apppull/1715/head
parent
462ec04091
commit
f906b57d4e
@ -1,20 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import androidx.core.util.Pair
|
||||
import com.etesync.journalmanager.UserInfoManager.UserInfo
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.tasks.ui.CompletableViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("use etebase")
|
||||
@HiltViewModel
|
||||
class AddEteSyncAccountViewModel @Inject constructor(
|
||||
private val client: EteSyncClient): CompletableViewModel<Pair<UserInfo, String>>() {
|
||||
suspend fun addAccount(url: String, username: String, password: String) {
|
||||
run {
|
||||
client.setForeground()
|
||||
val token = client.forUrl(url, username, null, null).getToken(password)
|
||||
Pair.create(client.forUrl(url, username, null, token!!).userInfo(), token)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.tasks.data.CaldavAccount
|
||||
import org.tasks.ui.CompletableViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("use etebase")
|
||||
@HiltViewModel
|
||||
class CreateUserInfoViewModel @Inject constructor(
|
||||
private val client: EteSyncClient): CompletableViewModel<String>() {
|
||||
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {
|
||||
run {
|
||||
client.forAccount(caldavAccount).createUserInfo(derivedKey)
|
||||
derivedKey
|
||||
}
|
||||
}
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
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.extensions.Context.openUri
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity
|
||||
import org.tasks.security.KeyStoreEncryption
|
||||
import org.tasks.ui.DisplayableException
|
||||
import java.net.ConnectException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("use etebase")
|
||||
@AndroidEntryPoint
|
||||
class EncryptionSettingsActivity : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
@Inject lateinit var encryption: KeyStoreEncryption
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
binding.repeatEncryptionPassword.addTextChangedListener(
|
||||
onTextChanged = { _, _, _, _ -> onRepeatEncryptionPasswordChanged() }
|
||||
)
|
||||
binding.encryptionPassword.addTextChangedListener(
|
||||
onTextChanged = { _, _, _, _ -> onEncryptionPasswordChanged() }
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private fun onRepeatEncryptionPasswordChanged() {
|
||||
binding.repeatEncryptionPasswordLayout.error = null
|
||||
}
|
||||
|
||||
private 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) {
|
||||
openUri(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,277 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.util.Pair
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import com.etesync.journalmanager.*
|
||||
import com.etesync.journalmanager.Constants.Companion.CURRENT_VERSION
|
||||
import com.etesync.journalmanager.Crypto.AsymmetricKeyPair
|
||||
import com.etesync.journalmanager.Crypto.CryptoManager
|
||||
import com.etesync.journalmanager.Exceptions.IntegrityException
|
||||
import com.etesync.journalmanager.Exceptions.VersionTooNewException
|
||||
import com.etesync.journalmanager.JournalManager.Journal
|
||||
import com.etesync.journalmanager.UserInfoManager.UserInfo.Companion.generate
|
||||
import com.etesync.journalmanager.model.CollectionInfo
|
||||
import com.etesync.journalmanager.model.CollectionInfo.Companion.fromJson
|
||||
import com.etesync.journalmanager.model.SyncEntry
|
||||
import com.etesync.journalmanager.util.TokenAuthenticator
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.tls.OkHostnameVerifier
|
||||
import org.tasks.DebugNetworkInterceptor
|
||||
import org.tasks.caldav.MemoryCookieStore
|
||||
import org.tasks.data.CaldavAccount
|
||||
import org.tasks.data.CaldavCalendar
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.security.KeyStoreEncryption
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.net.ssl.SSLContext
|
||||
|
||||
@Deprecated("use etebase")
|
||||
class EteSyncClient {
|
||||
private val encryption: KeyStoreEncryption
|
||||
private val preferences: Preferences
|
||||
private val interceptor: DebugNetworkInterceptor
|
||||
private val username: String?
|
||||
private val token: String?
|
||||
private val encryptionPassword: String?
|
||||
private val httpClient: OkHttpClient?
|
||||
private val httpUrl: HttpUrl?
|
||||
private val context: Context
|
||||
private val journalManager: JournalManager?
|
||||
private var foreground = false
|
||||
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
encryption: KeyStoreEncryption,
|
||||
preferences: Preferences,
|
||||
interceptor: DebugNetworkInterceptor) {
|
||||
this.context = context
|
||||
this.encryption = encryption
|
||||
this.preferences = preferences
|
||||
this.interceptor = interceptor
|
||||
username = null
|
||||
token = null
|
||||
encryptionPassword = null
|
||||
httpClient = null
|
||||
httpUrl = null
|
||||
journalManager = null
|
||||
}
|
||||
|
||||
private constructor(
|
||||
context: Context,
|
||||
encryption: KeyStoreEncryption,
|
||||
preferences: Preferences,
|
||||
interceptor: DebugNetworkInterceptor,
|
||||
url: String?,
|
||||
username: String?,
|
||||
encryptionPassword: String?,
|
||||
token: String?,
|
||||
foreground: Boolean) {
|
||||
this.context = context
|
||||
this.encryption = encryption
|
||||
this.preferences = preferences
|
||||
this.interceptor = interceptor
|
||||
this.username = username
|
||||
this.encryptionPassword = encryptionPassword
|
||||
this.token = token
|
||||
this.foreground = foreground
|
||||
val customCertManager = CustomCertManager(context)
|
||||
customCertManager.appInForeground = foreground
|
||||
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, arrayOf(customCertManager), null)
|
||||
val builder = OkHttpClient()
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(TokenAuthenticator(null, token))
|
||||
.cookieJar(MemoryCookieStore())
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(true)
|
||||
.sslSocketFactory(sslContext.socketFactory, customCertManager)
|
||||
.hostnameVerifier(hostnameVerifier)
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
if (preferences.isFlipperEnabled) {
|
||||
interceptor.apply(builder)
|
||||
}
|
||||
httpClient = builder.build()
|
||||
httpUrl = url?.toHttpUrlOrNull()
|
||||
journalManager = JournalManager(httpClient, httpUrl!!)
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
|
||||
suspend fun forAccount(account: CaldavAccount): EteSyncClient {
|
||||
return forUrl(
|
||||
account.url,
|
||||
account.username,
|
||||
account.getEncryptionPassword(encryption),
|
||||
account.getPassword(encryption))
|
||||
}
|
||||
|
||||
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
|
||||
suspend fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteSyncClient = withContext(Dispatchers.IO) {
|
||||
EteSyncClient(
|
||||
context,
|
||||
encryption,
|
||||
preferences,
|
||||
interceptor,
|
||||
url,
|
||||
username,
|
||||
encryptionPassword,
|
||||
token,
|
||||
foreground)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, Exceptions.HttpException::class)
|
||||
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
|
||||
JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!)
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class)
|
||||
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) {
|
||||
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!)
|
||||
userInfoManager.fetch(username!!)
|
||||
}
|
||||
|
||||
@Throws(VersionTooNewException::class, IntegrityException::class)
|
||||
fun getCrypto(userInfo: UserInfoManager.UserInfo?, journal: Journal): CryptoManager {
|
||||
if (journal.key == null) {
|
||||
return CryptoManager(journal.version, encryptionPassword!!, journal.uid!!)
|
||||
}
|
||||
if (userInfo == null) {
|
||||
throw RuntimeException("Missing userInfo")
|
||||
}
|
||||
val cryptoManager = CryptoManager(userInfo.version!!.toInt(), encryptionPassword!!, "userInfo")
|
||||
val keyPair = AsymmetricKeyPair(userInfo.getContent(cryptoManager)!!, userInfo.pubkey!!)
|
||||
return CryptoManager(journal.version, keyPair, journal.key!!)
|
||||
}
|
||||
|
||||
private fun convertJournalToCollection(userInfo: UserInfoManager.UserInfo?, journal: Journal): CollectionInfo? {
|
||||
return try {
|
||||
val cryptoManager = getCrypto(userInfo, journal)
|
||||
journal.verify(cryptoManager)
|
||||
val collection = fromJson(journal.getContent(cryptoManager))
|
||||
collection.updateFromJournal(journal)
|
||||
collection
|
||||
} catch (e: IntegrityException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
} catch (e: VersionTooNewException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class)
|
||||
suspend fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> = withContext(Dispatchers.IO) {
|
||||
val result: MutableMap<Journal, CollectionInfo> = HashMap()
|
||||
for (journal in journalManager!!.list()) {
|
||||
val collection = convertJournalToCollection(userInfo, journal)
|
||||
if (collection != null) {
|
||||
if (TYPE_TASKS == collection.type) {
|
||||
Timber.v("Found %s", collection)
|
||||
result[journal] = collection
|
||||
} else {
|
||||
Timber.v("Ignoring %s", collection)
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
|
||||
suspend fun getSyncEntries(
|
||||
userInfo: UserInfoManager.UserInfo?,
|
||||
journal: Journal,
|
||||
calendar: CaldavCalendar,
|
||||
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) {
|
||||
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
|
||||
val crypto = getCrypto(userInfo, journal)
|
||||
var journalEntries: List<JournalEntryManager.Entry>
|
||||
do {
|
||||
journalEntries = journalEntryManager.list(crypto, calendar.ctag, MAX_FETCH)
|
||||
callback(journalEntries.map {
|
||||
Pair.create(it, SyncEntry.fromJournalEntry(crypto, it))
|
||||
})
|
||||
} while (journalEntries.size >= MAX_FETCH)
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class)
|
||||
suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) {
|
||||
var remoteCtag = remoteCtag
|
||||
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
|
||||
for (partition in entries!!.chunked(MAX_PUSH)) {
|
||||
journalEntryManager.create(partition, remoteCtag)
|
||||
remoteCtag = partition[partition.size - 1].uid
|
||||
}
|
||||
}
|
||||
|
||||
fun setForeground() {
|
||||
foreground = true
|
||||
}
|
||||
|
||||
suspend fun invalidateToken() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
|
||||
suspend fun makeCollection(name: String?, color: Int): String = withContext(Dispatchers.IO) {
|
||||
val uid = Journal.genUid()
|
||||
val collectionInfo = CollectionInfo()
|
||||
collectionInfo.displayName = name
|
||||
collectionInfo.type = TYPE_TASKS
|
||||
collectionInfo.uid = uid
|
||||
collectionInfo.selected = true
|
||||
collectionInfo.color = if (color == 0) null else color
|
||||
val crypto = CryptoManager(collectionInfo.version, encryptionPassword!!, uid)
|
||||
journalManager!!.create(Journal(crypto, collectionInfo.toJson(), uid))
|
||||
uid
|
||||
}
|
||||
|
||||
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
|
||||
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? = withContext(Dispatchers.IO) {
|
||||
val uid = calendar.url
|
||||
val journal = journalManager!!.fetch(uid!!)
|
||||
val userInfo = userInfo()
|
||||
val crypto = getCrypto(userInfo, journal)
|
||||
val collectionInfo = convertJournalToCollection(userInfo, journal)
|
||||
collectionInfo!!.displayName = name
|
||||
collectionInfo.color = if (color == 0) null else color
|
||||
journalManager.update(Journal(crypto, collectionInfo.toJson(), uid))
|
||||
uid
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class)
|
||||
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) {
|
||||
journalManager!!.delete(Journal.fakeWithUid(calendar.url!!))
|
||||
}
|
||||
|
||||
@Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class)
|
||||
suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) {
|
||||
val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo")
|
||||
val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!)
|
||||
UserInfoManager(httpClient!!, httpUrl!!).create(userInfo)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TYPE_TASKS = "TASKS"
|
||||
private const val MAX_FETCH = 50
|
||||
private const val MAX_PUSH = 30
|
||||
}
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.util.Pair
|
||||
import at.bitfire.ical4android.ICalendar.Companion.prodId
|
||||
import com.etesync.journalmanager.Exceptions
|
||||
import com.etesync.journalmanager.Exceptions.IntegrityException
|
||||
import com.etesync.journalmanager.Exceptions.VersionTooNewException
|
||||
import com.etesync.journalmanager.JournalEntryManager
|
||||
import com.etesync.journalmanager.JournalEntryManager.Entry.Companion.getFakeWithUid
|
||||
import com.etesync.journalmanager.JournalManager.Journal
|
||||
import com.etesync.journalmanager.UserInfoManager
|
||||
import com.etesync.journalmanager.model.SyncEntry
|
||||
import com.todoroo.astrid.helper.UUIDHelper
|
||||
import com.todoroo.astrid.service.TaskDeleter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import net.fortuna.ical4j.model.property.ProdId
|
||||
import org.tasks.BuildConfig
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.Strings.isNullOrEmpty
|
||||
import org.tasks.billing.Inventory
|
||||
import org.tasks.caldav.iCalendar
|
||||
import org.tasks.caldav.iCalendar.Companion.fromVtodo
|
||||
import org.tasks.data.CaldavAccount
|
||||
import org.tasks.data.CaldavCalendar
|
||||
import org.tasks.data.CaldavDao
|
||||
import org.tasks.data.CaldavTaskContainer
|
||||
import timber.log.Timber
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
@Deprecated("use etebase")
|
||||
class EteSynchronizer @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val caldavDao: CaldavDao,
|
||||
private val localBroadcastManager: LocalBroadcastManager,
|
||||
private val taskDeleter: TaskDeleter,
|
||||
private val inventory: Inventory,
|
||||
private val client: EteSyncClient,
|
||||
private val iCal: iCalendar) {
|
||||
companion object {
|
||||
init {
|
||||
prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sync(account: CaldavAccount) {
|
||||
Thread.currentThread().contextClassLoader = context.classLoader
|
||||
|
||||
if (!inventory.hasPro) {
|
||||
setError(account, context.getString(R.string.requires_pro_subscription))
|
||||
return
|
||||
}
|
||||
if (isNullOrEmpty(account.password)) {
|
||||
setError(account, context.getString(R.string.password_required))
|
||||
return
|
||||
}
|
||||
if (isNullOrEmpty(account.encryptionKey)) {
|
||||
setError(account, context.getString(R.string.encryption_password_required))
|
||||
return
|
||||
}
|
||||
try {
|
||||
synchronize(account)
|
||||
} catch (e: KeyManagementException) {
|
||||
setError(account, e.message)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
setError(account, e.message)
|
||||
} catch (e: Exceptions.HttpException) {
|
||||
setError(account, e.message)
|
||||
} catch (e: IntegrityException) {
|
||||
setError(account, e.message)
|
||||
} catch (e: VersionTooNewException) {
|
||||
setError(account, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
|
||||
private suspend fun synchronize(account: CaldavAccount) {
|
||||
val client = client.forAccount(account)
|
||||
val userInfo = client.userInfo()
|
||||
val resources = client.getCalendars(userInfo)
|
||||
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()
|
||||
Timber.d("Found uids: %s", uids)
|
||||
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, uids.toList())) {
|
||||
taskDeleter.delete(calendar)
|
||||
}
|
||||
for ((key, collection) in resources) {
|
||||
val uid = collection.uid
|
||||
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, uid!!)
|
||||
val colorInt = collection.color
|
||||
val color = colorInt ?: 0
|
||||
if (calendar == null) {
|
||||
calendar = CaldavCalendar()
|
||||
calendar.name = collection.displayName
|
||||
calendar.account = account.uuid
|
||||
calendar.url = collection.uid
|
||||
calendar.uuid = UUIDHelper.newUUID()
|
||||
calendar.color = color
|
||||
caldavDao.insert(calendar)
|
||||
} else {
|
||||
if (calendar.name != collection.displayName
|
||||
|| calendar.color != color) {
|
||||
calendar.name = collection.displayName
|
||||
calendar.color = color
|
||||
caldavDao.update(calendar)
|
||||
localBroadcastManager.broadcastRefreshList()
|
||||
}
|
||||
}
|
||||
sync(client, userInfo!!, calendar, key)
|
||||
}
|
||||
setError(account, "")
|
||||
}
|
||||
|
||||
private suspend fun setError(account: CaldavAccount, message: String?) {
|
||||
account.error = message
|
||||
caldavDao.update(account)
|
||||
localBroadcastManager.broadcastRefreshList()
|
||||
if (!isNullOrEmpty(message)) {
|
||||
Timber.e(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
|
||||
private suspend fun sync(
|
||||
client: EteSyncClient,
|
||||
userInfo: UserInfoManager.UserInfo,
|
||||
caldavCalendar: CaldavCalendar,
|
||||
journal: Journal) {
|
||||
Timber.d("sync(%s)", caldavCalendar)
|
||||
val localChanges = HashMap<String?, CaldavTaskContainer>()
|
||||
for (task in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) {
|
||||
localChanges[task.remoteId] = task
|
||||
}
|
||||
var remoteCtag = journal.lastUid
|
||||
if (isNullOrEmpty(remoteCtag) || remoteCtag != caldavCalendar.ctag) {
|
||||
Timber.v("Applying remote changes")
|
||||
client.getSyncEntries(userInfo, journal, caldavCalendar) {
|
||||
applyEntries(caldavCalendar, it, localChanges.keys)
|
||||
}
|
||||
} else {
|
||||
Timber.d("%s up to date", caldavCalendar.name)
|
||||
}
|
||||
val changes: MutableList<SyncEntry> = ArrayList()
|
||||
for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) {
|
||||
val vtodo = task.vtodo
|
||||
if (!isNullOrEmpty(vtodo)) {
|
||||
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE))
|
||||
}
|
||||
}
|
||||
for (task in localChanges.values) {
|
||||
val vtodo = task.vtodo
|
||||
val existingTask = !isNullOrEmpty(vtodo)
|
||||
if (task.isDeleted) {
|
||||
if (existingTask) {
|
||||
changes.add(SyncEntry(vtodo!!, SyncEntry.Actions.DELETE))
|
||||
}
|
||||
} else {
|
||||
changes.add(
|
||||
SyncEntry(
|
||||
String(iCal.toVtodo(task.caldavTask, task.task)),
|
||||
if (existingTask) SyncEntry.Actions.CHANGE else SyncEntry.Actions.ADD))
|
||||
}
|
||||
}
|
||||
remoteCtag = caldavCalendar.ctag
|
||||
val crypto = client.getCrypto(userInfo, journal)
|
||||
val updates: MutableList<Pair<JournalEntryManager.Entry, SyncEntry>> = ArrayList()
|
||||
var previous: JournalEntryManager.Entry? = if (isNullOrEmpty(remoteCtag)) null else getFakeWithUid(remoteCtag!!)
|
||||
for (syncEntry in changes) {
|
||||
val entry = JournalEntryManager.Entry()
|
||||
entry.update(crypto, syncEntry.toJson(), previous)
|
||||
updates.add(Pair.create(entry, syncEntry))
|
||||
previous = entry
|
||||
}
|
||||
if (updates.size > 0) {
|
||||
Timber.v("Pushing local changes")
|
||||
client.pushEntries(journal, updates.mapNotNull { it.first }, remoteCtag)
|
||||
Timber.v("Applying local changes")
|
||||
applyEntries(caldavCalendar, updates, HashSet())
|
||||
}
|
||||
Timber.d("UPDATE %s", caldavCalendar)
|
||||
caldavDao.update(caldavCalendar)
|
||||
caldavDao.updateParents(caldavCalendar.uuid!!)
|
||||
localBroadcastManager.broadcastRefresh()
|
||||
}
|
||||
|
||||
private suspend fun applyEntries(
|
||||
caldavCalendar: CaldavCalendar,
|
||||
syncEntries: List<Pair<JournalEntryManager.Entry, SyncEntry>>,
|
||||
dirty: MutableSet<String?>) {
|
||||
for (entry in syncEntries) {
|
||||
val journalEntry = entry.first
|
||||
val syncEntry = entry.second
|
||||
val action = syncEntry!!.action
|
||||
val vtodo = syncEntry.content
|
||||
Timber.v("%s: %s", action, vtodo)
|
||||
val task = fromVtodo(vtodo) ?: continue
|
||||
val remoteId = task.uid
|
||||
val caldavTask = caldavDao.getTaskByRemoteId(caldavCalendar.uuid!!, remoteId!!)
|
||||
when (action) {
|
||||
SyncEntry.Actions.ADD, SyncEntry.Actions.CHANGE -> if (dirty.contains(remoteId)) {
|
||||
caldavTask!!.vtodo = vtodo
|
||||
caldavDao.update(caldavTask)
|
||||
} else {
|
||||
iCal.fromVtodo(caldavCalendar, caldavTask, task, vtodo, null, null)
|
||||
}
|
||||
SyncEntry.Actions.DELETE -> {
|
||||
dirty.remove(remoteId)
|
||||
if (caldavTask != null) {
|
||||
if (caldavTask.isDeleted()) {
|
||||
caldavDao.delete(caldavTask)
|
||||
} else {
|
||||
taskDeleter.delete(caldavTask.task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
caldavCalendar.ctag = journalEntry!!.uid
|
||||
caldavDao.update(caldavCalendar)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package org.tasks.etesync
|
||||
|
||||
import androidx.core.util.Pair
|
||||
import com.etesync.journalmanager.UserInfoManager.UserInfo
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.tasks.Strings.isNullOrEmpty
|
||||
import org.tasks.ui.CompletableViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@Deprecated("use etebase")
|
||||
@HiltViewModel
|
||||
class UpdateEteSyncAccountViewModel @Inject constructor(
|
||||
private val client: EteSyncClient) : CompletableViewModel<Pair<UserInfo, String>>() {
|
||||
suspend fun updateAccount(url: String, user: String, pass: String?, token: String) {
|
||||
run {
|
||||
client.setForeground()
|
||||
if (isNullOrEmpty(pass)) {
|
||||
Pair.create(client.forUrl(url, user, null, token).userInfo(), token)
|
||||
} else {
|
||||
val newToken = client.forUrl(url, user, null, null).getToken(pass)
|
||||
Pair.create(client.forUrl(url, user, null, newToken).userInfo(), newToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.*
|
||||
import org.tasks.LocalBroadcastManager
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
|
||||
import org.tasks.data.CaldavDao
|
||||
import org.tasks.etesync.EteSynchronizer
|
||||
import org.tasks.preferences.Preferences
|
||||
|
||||
@Deprecated("use etebase")
|
||||
@HiltWorker
|
||||
class SyncEteSyncWork @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
firebase: Firebase,
|
||||
localBroadcastManager: LocalBroadcastManager,
|
||||
preferences: Preferences,
|
||||
private val caldavDao: CaldavDao,
|
||||
private val eteSynchronizer: EteSynchronizer
|
||||
) : SyncWork(context, workerParams, firebase, localBroadcastManager, preferences) {
|
||||
|
||||
override suspend fun enabled() = caldavDao.getAccounts(TYPE_ETESYNC).isNotEmpty()
|
||||
|
||||
override val syncStatus = R.string.p_sync_ongoing_etesync
|
||||
|
||||
override suspend fun doSync() {
|
||||
firebase.logEvent(R.string.legacy_etesync)
|
||||
etesyncJobs().awaitAll()
|
||||
}
|
||||
|
||||
private suspend fun etesyncJobs(): List<Deferred<Unit>> = coroutineScope {
|
||||
caldavDao.getAccounts(TYPE_ETESYNC)
|
||||
.map {
|
||||
async(Dispatchers.IO) {
|
||||
eteSynchronizer.sync(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/root_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar" />
|
||||
|
||||
<include
|
||||
android:id="@+id/progress_bar"
|
||||
layout="@layout/progress_view" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
style="@style/TagSettingsRow"
|
||||
android:text="@string/etesync_encryption_description"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/encryption_password_layout"
|
||||
style="@style/TagSettingsRow"
|
||||
android:hint="@string/encryption_password"
|
||||
app:endIconMode="password_toggle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/encryption_password"
|
||||
style="@style/TagSettingsEditText"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/repeat_encryption_password_layout"
|
||||
style="@style/TagSettingsRow"
|
||||
android:visibility="gone"
|
||||
app:endIconMode="password_toggle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/repeat_encryption_password"
|
||||
style="@style/TagSettingsEditText"
|
||||
android:hint="@string/reenter_encryption_password"
|
||||
android:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
Loading…
Reference in New Issue