diff --git a/app/src/main/java/org/tasks/etebase/AddEteBaseAccountViewModel.kt b/app/src/main/java/org/tasks/etebase/AddEteBaseAccountViewModel.kt index 30deac0aa..f3658c78b 100644 --- a/app/src/main/java/org/tasks/etebase/AddEteBaseAccountViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/AddEteBaseAccountViewModel.kt @@ -6,12 +6,18 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo import org.tasks.ui.CompletableViewModel class AddEteBaseAccountViewModel @ViewModelInject constructor( - private val client: EteBaseClient): CompletableViewModel>() { + private val clientProvider: EteBaseClientProvider): CompletableViewModel>() { 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 + ) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/CreateCalendarViewModel.kt b/app/src/main/java/org/tasks/etebase/CreateCalendarViewModel.kt index dfd4d5b43..d5048b6cb 100644 --- a/app/src/main/java/org/tasks/etebase/CreateCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/CreateCalendarViewModel.kt @@ -5,8 +5,8 @@ import org.tasks.data.CaldavAccount import org.tasks.ui.CompletableViewModel class CreateCalendarViewModel @ViewModelInject constructor( - private val client: EteBaseClient) : CompletableViewModel() { + private val clientProvider: EteBaseClientProvider) : CompletableViewModel() { suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) { - run { client.forAccount(account).makeCollection(name, color) } + run { clientProvider.forAccount(account).makeCollection(name, color) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/CreateUserInfoViewModel.kt b/app/src/main/java/org/tasks/etebase/CreateUserInfoViewModel.kt index 02a7f5bd1..d7ca15277 100644 --- a/app/src/main/java/org/tasks/etebase/CreateUserInfoViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/CreateUserInfoViewModel.kt @@ -5,10 +5,10 @@ import org.tasks.data.CaldavAccount import org.tasks.ui.CompletableViewModel class CreateUserInfoViewModel @ViewModelInject constructor( - private val client: EteBaseClient): CompletableViewModel() { + private val clientProvider: EteBaseClientProvider): CompletableViewModel() { suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) { run { - client.forAccount(caldavAccount).createUserInfo(derivedKey) + clientProvider.forAccount(caldavAccount).createUserInfo(derivedKey) derivedKey } } diff --git a/app/src/main/java/org/tasks/etebase/DeleteCalendarViewModel.kt b/app/src/main/java/org/tasks/etebase/DeleteCalendarViewModel.kt index 0c4906e61..681b4b8bd 100644 --- a/app/src/main/java/org/tasks/etebase/DeleteCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/DeleteCalendarViewModel.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/EteBaseAccountSettingsActivity.kt b/app/src/main/java/org/tasks/etebase/EteBaseAccountSettingsActivity.kt index 37cd0246c..6e1091366 100644 --- a/app/src/main/java/org/tasks/etebase/EteBaseAccountSettingsActivity.kt +++ b/app/src/main/java/org/tasks/etebase/EteBaseAccountSettingsActivity.kt @@ -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() } diff --git a/app/src/main/java/org/tasks/etebase/EteBaseClient.kt b/app/src/main/java/org/tasks/etebase/EteBaseClient.kt index 8db849a98..ffb567fb8 100644 --- a/app/src/main/java/org/tasks/etebase/EteBaseClient.kt +++ b/app/src/main/java/org/tasks/etebase/EteBaseClient.kt @@ -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 = withContext(Dispatchers.IO) { val result: MutableMap = 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>) -> 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 do { @@ -211,20 +110,21 @@ class EteBaseClient { @Throws(Exceptions.HttpException::class) suspend fun pushEntries(journal: Journal, entries: List?, 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 { diff --git a/app/src/main/java/org/tasks/etebase/EteBaseClientProvider.kt b/app/src/main/java/org/tasks/etebase/EteBaseClientProvider.kt new file mode 100644 index 000000000..1c471ba9e --- /dev/null +++ b/app/src/main/java/org/tasks/etebase/EteBaseClientProvider.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/EteBaseSynchronizer.kt b/app/src/main/java/org/tasks/etebase/EteBaseSynchronizer.kt index 7b5b9f336..184da6cb5 100644 --- a/app/src/main/java/org/tasks/etebase/EteBaseSynchronizer.kt +++ b/app/src/main/java/org/tasks/etebase/EteBaseSynchronizer.kt @@ -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 = resources.values.mapNotNull { it.uid }.toHashSet() diff --git a/app/src/main/java/org/tasks/etebase/UpdateCalendarViewModel.kt b/app/src/main/java/org/tasks/etebase/UpdateCalendarViewModel.kt index d234cb040..a9664a00d 100644 --- a/app/src/main/java/org/tasks/etebase/UpdateCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/UpdateCalendarViewModel.kt @@ -6,8 +6,8 @@ import org.tasks.data.CaldavCalendar import org.tasks.ui.CompletableViewModel class UpdateCalendarViewModel @ViewModelInject constructor( - private val client: EteBaseClient): CompletableViewModel() { + private val clientProvider: EteBaseClientProvider): CompletableViewModel() { 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) } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/UpdateEteBaseAccountViewModel.kt b/app/src/main/java/org/tasks/etebase/UpdateEteBaseAccountViewModel.kt index 37497595c..80cacab90 100644 --- a/app/src/main/java/org/tasks/etebase/UpdateEteBaseAccountViewModel.kt +++ b/app/src/main/java/org/tasks/etebase/UpdateEteBaseAccountViewModel.kt @@ -7,15 +7,24 @@ import org.tasks.Strings.isNullOrEmpty import org.tasks.ui.CompletableViewModel class UpdateEteBaseAccountViewModel @ViewModelInject constructor( - private val client: EteBaseClient) : CompletableViewModel>() { + private val clientProvider: EteBaseClientProvider) : CompletableViewModel>() { 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 + ) } } }