Add EteBaseClientProvider

pull/1244/head
Alex Baker 5 years ago
parent af2213d60f
commit 9470eb2786

@ -6,12 +6,18 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.ui.CompletableViewModel
class AddEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<Pair<UserInfo, String>>() {
private val clientProvider: EteBaseClientProvider): 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)
val token =
clientProvider
.forUrl(url, username, null, null)
.setForeground()
.getToken(password)
Pair.create(
clientProvider.forUrl(url, username, null, token!!).userInfo(),
token
)
}
}
}

@ -5,8 +5,8 @@ import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
class CreateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<String?>() {
private val clientProvider: EteBaseClientProvider) : CompletableViewModel<String?>() {
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {
run { client.forAccount(account).makeCollection(name, color) }
run { clientProvider.forAccount(account).makeCollection(name, color) }
}
}

@ -5,10 +5,10 @@ import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel
class CreateUserInfoViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String>() {
private val clientProvider: EteBaseClientProvider): CompletableViewModel<String>() {
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {
run {
client.forAccount(caldavAccount).createUserInfo(derivedKey)
clientProvider.forAccount(caldavAccount).createUserInfo(derivedKey)
derivedKey
}
}

@ -6,8 +6,8 @@ import org.tasks.data.CaldavCalendar
import org.tasks.ui.ActionViewModel
class DeleteCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : ActionViewModel() {
private val clientProvider: EteBaseClientProvider) : ActionViewModel() {
suspend fun deleteCalendar(account: CaldavAccount, calendar: CaldavCalendar) {
run { client.forAccount(account).deleteCollection(calendar) }
run { clientProvider.forAccount(account).deleteCollection(calendar) }
}
}

@ -27,7 +27,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var eteBaseClient: EteBaseClient
@Inject lateinit var clientProvider: EteBaseClientProvider
private val addAccountViewModel: AddEteBaseAccountViewModel by viewModels()
private val updateAccountViewModel: UpdateEteBaseAccountViewModel by viewModels()
@ -173,7 +173,7 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
}
override suspend fun removeAccount() {
caldavAccount?.let { eteBaseClient.forAccount(it).invalidateToken() }
caldavAccount?.let { clientProvider.forAccount(it).invalidateToken() }
super.removeAccount()
}

@ -1,6 +1,5 @@
package org.tasks.etebase
import android.content.Context
import androidx.core.util.Pair
import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.*
@ -14,134 +13,34 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo.Companion.generate
import com.etesync.journalmanager.model.CollectionInfo
import com.etesync.journalmanager.model.CollectionInfo.Companion.fromJson
import com.etesync.journalmanager.model.SyncEntry
import com.etesync.journalmanager.util.TokenAuthenticator
import com.google.common.collect.Lists
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.MemoryCookieStore
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import timber.log.Timber
import java.io.IOException
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
class EteBaseClient {
private val encryption: KeyStoreEncryption
private val preferences: Preferences
private val interceptor: DebugNetworkInterceptor
private val username: String?
private val token: String?
private val encryptionPassword: String?
private val httpClient: OkHttpClient?
private val httpUrl: HttpUrl?
private val context: Context
private val journalManager: JournalManager?
private var foreground = false
@Inject
constructor(
@ApplicationContext context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
username = null
token = null
encryptionPassword = null
httpClient = null
httpUrl = null
journalManager = null
}
private constructor(
context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor,
url: String?,
username: String?,
encryptionPassword: String?,
token: String?,
foreground: Boolean) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
this.username = username
this.encryptionPassword = encryptionPassword
this.token = token
this.foreground = foreground
val customCertManager = CustomCertManager(context)
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(customCertManager), null)
val builder = OkHttpClient()
.newBuilder()
.addNetworkInterceptor(TokenAuthenticator(null, token))
.cookieJar(MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.socketFactory, customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
if (preferences.isFlipperEnabled) {
interceptor.apply(builder)
}
httpClient = builder.build()
httpUrl = url?.toHttpUrlOrNull()
journalManager = JournalManager(httpClient, httpUrl!!)
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl(
account.url,
account.username,
account.getEncryptionPassword(encryption),
account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) {
EteBaseClient(
context,
encryption,
preferences,
interceptor,
url,
username,
encryptionPassword,
token,
foreground)
}
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 journalManager = JournalManager(httpClient, httpUrl)
@Throws(IOException::class, Exceptions.HttpException::class)
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!)
JournalAuthenticator(httpClient, httpUrl).getAuthToken(username!!, password!!)
}
@Throws(Exceptions.HttpException::class)
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) {
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!)
val userInfoManager = UserInfoManager(httpClient, httpUrl)
userInfoManager.fetch(username!!)
}
@ -177,7 +76,7 @@ class EteBaseClient {
@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()) {
for (journal in journalManager.list()) {
val collection = convertJournalToCollection(userInfo, journal)
if (collection != null) {
if (TYPE_TASKS == collection.type) {
@ -197,7 +96,7 @@ class EteBaseClient {
journal: Journal,
calendar: CaldavCalendar,
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) {
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!)
val crypto = getCrypto(userInfo, journal)
var journalEntries: List<JournalEntryManager.Entry>
do {
@ -211,20 +110,21 @@ class EteBaseClient {
@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!!)
val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!)
for (partition in Lists.partition(entries!!, MAX_PUSH)) {
journalEntryManager.create(partition, remoteCtag)
remoteCtag = partition[partition.size - 1].uid
}
}
fun setForeground() {
foreground = true
fun setForeground(): EteBaseClient {
customCertManager.appInForeground = true
return this
}
suspend fun invalidateToken() = withContext(Dispatchers.IO) {
try {
JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!)
JournalAuthenticator(httpClient, httpUrl).invalidateAuthToken(token!!)
} catch (e: Exception) {
Timber.e(e)
}
@ -240,14 +140,14 @@ class EteBaseClient {
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))
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) {
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String = withContext(Dispatchers.IO) {
val uid = calendar.url
val journal = journalManager!!.fetch(uid!!)
val journal = journalManager.fetch(uid!!)
val userInfo = userInfo()
val crypto = getCrypto(userInfo, journal)
val collectionInfo = convertJournalToCollection(userInfo, journal)
@ -259,14 +159,14 @@ class EteBaseClient {
@Throws(Exceptions.HttpException::class)
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) {
journalManager!!.delete(Journal.fakeWithUid(calendar.url!!))
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)
UserInfoManager(httpClient, httpUrl).create(userInfo)
}
companion object {

@ -0,0 +1,80 @@
package org.tasks.etebase
import android.content.Context
import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.util.TokenAuthenticator
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
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.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
class EteBaseClientProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor
) {
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl(
account.url!!,
account.username,
account.getEncryptionPassword(encryption),
account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) {
val customCertManager = newCertManager()
EteBaseClient(
customCertManager,
username,
encryptionPassword,
token,
createHttpClient(token, customCertManager),
url.toHttpUrl()
)
}
private suspend fun newCertManager() = withContext(Dispatchers.Default) {
CustomCertManager(context)
}
private fun createHttpClient(
token: String?,
customCertManager: CustomCertManager,
foreground: Boolean = false
): OkHttpClient {
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)
}
return builder.build()
}
}

@ -39,7 +39,7 @@ class EteBaseSynchronizer @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager,
private val taskDeleter: TaskDeleter,
private val inventory: Inventory,
private val client: EteBaseClient,
private val clientProvider: EteBaseClientProvider,
private val iCal: iCalendar) {
companion object {
init {
@ -79,7 +79,7 @@ class EteBaseSynchronizer @Inject constructor(
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
private suspend fun synchronize(account: CaldavAccount) {
val client = client.forAccount(account)
val client = clientProvider.forAccount(account)
val userInfo = client.userInfo()
val resources = client.getCalendars(userInfo)
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()

@ -6,8 +6,8 @@ import org.tasks.data.CaldavCalendar
import org.tasks.ui.CompletableViewModel
class UpdateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String?>() {
private val clientProvider: EteBaseClientProvider): CompletableViewModel<String?>() {
suspend fun updateCalendar(account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) {
run { client.forAccount(account).updateCollection(calendar, name, color) }
run { clientProvider.forAccount(account).updateCollection(calendar, name, color) }
}
}

@ -7,15 +7,24 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.ui.CompletableViewModel
class UpdateEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<Pair<UserInfo, String>>() {
private val clientProvider: EteBaseClientProvider) : 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)
Pair.create(
clientProvider.forUrl(url, user, null, token).setForeground().userInfo(),
token
)
} else {
val newToken = client.forUrl(url, user, null, null).getToken(pass)
Pair.create(client.forUrl(url, user, null, newToken).userInfo(), newToken)
val newToken =
clientProvider
.forUrl(url, user, null, null)
.setForeground()
.getToken(pass)!!
Pair.create(
clientProvider.forUrl(url, user, null, newToken).userInfo(),
newToken
)
}
}
}

Loading…
Cancel
Save