From 378580b1e84906c221dad8398904b31b0aeef0fb Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Mon, 13 Jul 2020 15:00:37 -0500 Subject: [PATCH] Convert EteSynchronizer to Kotlin --- .../java/org/tasks/etesync/EteSyncClient.java | 288 ------------------ .../java/org/tasks/etesync/EteSyncClient.kt | 276 +++++++++++++++++ .../org/tasks/etesync/EteSynchronizer.java | 268 ---------------- .../java/org/tasks/etesync/EteSynchronizer.kt | 221 ++++++++++++++ 4 files changed, 497 insertions(+), 556 deletions(-) delete mode 100644 app/src/main/java/org/tasks/etesync/EteSyncClient.java create mode 100644 app/src/main/java/org/tasks/etesync/EteSyncClient.kt delete mode 100644 app/src/main/java/org/tasks/etesync/EteSynchronizer.java create mode 100644 app/src/main/java/org/tasks/etesync/EteSynchronizer.kt diff --git a/app/src/main/java/org/tasks/etesync/EteSyncClient.java b/app/src/main/java/org/tasks/etesync/EteSyncClient.java deleted file mode 100644 index 130b087eb..000000000 --- a/app/src/main/java/org/tasks/etesync/EteSyncClient.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.tasks.etesync; - -import static com.google.common.collect.Lists.partition; -import static com.google.common.collect.Lists.transform; - -import android.content.Context; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; -import at.bitfire.cert4android.CustomCertManager; -import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier; -import com.etesync.journalmanager.Constants; -import com.etesync.journalmanager.Crypto; -import com.etesync.journalmanager.Crypto.CryptoManager; -import com.etesync.journalmanager.Exceptions; -import com.etesync.journalmanager.Exceptions.HttpException; -import com.etesync.journalmanager.Exceptions.IntegrityException; -import com.etesync.journalmanager.Exceptions.VersionTooNewException; -import com.etesync.journalmanager.JournalAuthenticator; -import com.etesync.journalmanager.JournalEntryManager; -import com.etesync.journalmanager.JournalEntryManager.Entry; -import com.etesync.journalmanager.JournalManager; -import com.etesync.journalmanager.JournalManager.Journal; -import com.etesync.journalmanager.UserInfoManager; -import com.etesync.journalmanager.UserInfoManager.UserInfo; -import com.etesync.journalmanager.model.CollectionInfo; -import com.etesync.journalmanager.model.SyncEntry; -import com.etesync.journalmanager.util.TokenAuthenticator; -import dagger.hilt.android.qualifiers.ApplicationContext; -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClient.Builder; -import okhttp3.internal.tls.OkHostnameVerifier; -import org.tasks.Callback; -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; - -public class EteSyncClient { - - private static final String TYPE_TASKS = "TASKS"; - private static final int MAX_FETCH = 50; - private static final int MAX_PUSH = 30; - - private final KeyStoreEncryption encryption; - private final Preferences preferences; - private final DebugNetworkInterceptor interceptor; - private final String username; - private final String token; - private final String encryptionPassword; - private final OkHttpClient httpClient; - private final HttpUrl httpUrl; - private final Context context; - private final JournalManager journalManager; - private boolean foreground; - - @Inject - public EteSyncClient( - @ApplicationContext Context context, - KeyStoreEncryption encryption, - Preferences preferences, - DebugNetworkInterceptor interceptor) { - 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 EteSyncClient( - Context context, - KeyStoreEncryption encryption, - Preferences preferences, - DebugNetworkInterceptor interceptor, - String url, - String username, - String encryptionPassword, - String token, - boolean foreground) - throws NoSuchAlgorithmException, KeyManagementException { - this.context = context; - this.encryption = encryption; - this.preferences = preferences; - this.interceptor = interceptor; - this.username = username; - this.encryptionPassword = encryptionPassword; - this.token = token; - this.foreground = foreground; - - CustomCertManager customCertManager = new CustomCertManager(context); - customCertManager.setAppInForeground(foreground); - CustomHostnameVerifier hostnameVerifier = - customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE); - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, new TrustManager[] {customCertManager}, null); - - Builder builder = - new OkHttpClient() - .newBuilder() - .addNetworkInterceptor(new TokenAuthenticator(null, token)) - .cookieJar(new MemoryCookieStore()) - .followRedirects(false) - .followSslRedirects(true) - .sslSocketFactory(sslContext.getSocketFactory(), customCertManager) - .hostnameVerifier(hostnameVerifier) - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(120, TimeUnit.SECONDS); - if (preferences.isFlipperEnabled()) { - interceptor.add(builder); - } - httpClient = builder.build(); - httpUrl = HttpUrl.parse(url); - journalManager = new JournalManager(httpClient, httpUrl); - } - - public EteSyncClient forAccount(CaldavAccount account) - throws NoSuchAlgorithmException, KeyManagementException { - return forUrl( - account.getUrl(), - account.getUsername(), - account.getEncryptionPassword(encryption), - account.getPassword(encryption)); - } - - EteSyncClient forUrl(String url, String username, String encryptionPassword, String token) - throws KeyManagementException, NoSuchAlgorithmException { - return new EteSyncClient( - context, - encryption, - preferences, - interceptor, - url, - username, - encryptionPassword, - token, - foreground); - } - - String getToken(String password) throws IOException, HttpException { - return new JournalAuthenticator(httpClient, httpUrl).getAuthToken(username, password); - } - - UserInfo getUserInfo() throws HttpException { - UserInfoManager userInfoManager = new UserInfoManager(httpClient, httpUrl); - return userInfoManager.fetch(username); - } - - CryptoManager getCrypto(UserInfo userInfo, Journal journal) - throws VersionTooNewException, IntegrityException { - if (journal.getKey() == null) { - return new CryptoManager(journal.getVersion(), encryptionPassword, journal.getUid()); - } - if (userInfo == null) { - throw new RuntimeException("Missing userInfo"); - } - CryptoManager cryptoManager = new CryptoManager(userInfo.getVersion(), encryptionPassword, "userInfo"); - Crypto.AsymmetricKeyPair keyPair = - new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); - return new CryptoManager(journal.getVersion(), keyPair, journal.getKey()); - } - - private @Nullable CollectionInfo convertJournalToCollection(UserInfo userInfo, Journal journal) { - try { - CryptoManager cryptoManager = getCrypto(userInfo, journal); - journal.verify(cryptoManager); - CollectionInfo collection = - CollectionInfo.Companion.fromJson(journal.getContent(cryptoManager)); - collection.updateFromJournal(journal); - return collection; - } catch (IntegrityException | VersionTooNewException e) { - Timber.e(e); - return null; - } - } - - public Map getCalendars(UserInfo userInfo) - throws Exceptions.HttpException { - Map result = new HashMap<>(); - for (Journal journal : journalManager.list()) { - CollectionInfo collection = convertJournalToCollection(userInfo, journal); - if (collection != null) { - if (TYPE_TASKS.equals(collection.getType())) { - Timber.v("Found %s", collection); - result.put(journal, collection); - } else { - Timber.v("Ignoring %s", collection); - } - } - } - return result; - } - - void getSyncEntries( - UserInfo userInfo, - Journal journal, - CaldavCalendar calendar, - Callback>> callback) - throws IntegrityException, Exceptions.HttpException, VersionTooNewException { - JournalEntryManager journalEntryManager = - new JournalEntryManager(httpClient, httpUrl, journal.getUid()); - CryptoManager crypto = getCrypto(userInfo, journal); - List journalEntries; - do { - journalEntries = journalEntryManager.list(crypto, calendar.getCtag(), MAX_FETCH); - callback.call( - transform(journalEntries, e -> Pair.create(e, SyncEntry.fromJournalEntry(crypto, e)))); - } while (journalEntries.size() >= MAX_FETCH); - } - - void pushEntries(Journal journal, List entries, String remoteCtag) throws HttpException { - JournalEntryManager journalEntryManager = - new JournalEntryManager(httpClient, httpUrl, journal.getUid()); - for (List partition : partition(entries, MAX_PUSH)) { - journalEntryManager.create(partition, remoteCtag); - remoteCtag = partition.get(partition.size() - 1).getUid(); - } - } - - void setForeground() { - foreground = true; - } - - void invalidateToken() { - try { - new JournalAuthenticator(httpClient, httpUrl).invalidateAuthToken(token); - } catch (Exception e) { - Timber.e(e); - } - } - - String makeCollection(String name, int color) - throws VersionTooNewException, IntegrityException, HttpException { - String uid = Journal.genUid(); - CollectionInfo collectionInfo = new CollectionInfo(); - collectionInfo.setDisplayName(name); - collectionInfo.setType(TYPE_TASKS); - collectionInfo.setUid(uid); - collectionInfo.setSelected(true); - collectionInfo.setColor(color == 0 ? null : color); - CryptoManager crypto = new CryptoManager(collectionInfo.getVersion(), encryptionPassword, uid); - journalManager.create(new Journal(crypto, collectionInfo.toJson(), uid)); - return uid; - } - - String updateCollection(CaldavCalendar calendar, String name, int color) - throws VersionTooNewException, IntegrityException, HttpException { - String uid = calendar.getUrl(); - Journal journal = journalManager.fetch(uid); - UserInfo userInfo = getUserInfo(); - CryptoManager crypto = getCrypto(userInfo, journal); - CollectionInfo collectionInfo = convertJournalToCollection(userInfo, journal); - collectionInfo.setDisplayName(name); - collectionInfo.setColor(color == 0 ? null : color); - journalManager.update(new Journal(crypto, collectionInfo.toJson(), uid)); - return uid; - } - - void deleteCollection(CaldavCalendar calendar) throws HttpException { - journalManager.delete(Journal.fakeWithUid(calendar.getUrl())); - } - - void createUserInfo(String derivedKey) - throws HttpException, VersionTooNewException, IntegrityException, IOException { - CryptoManager cryptoManager = - new CryptoManager(Constants.CURRENT_VERSION, derivedKey, "userInfo"); - UserInfo userInfo = UserInfo.generate(cryptoManager, username); - new UserInfoManager(httpClient, httpUrl).create(userInfo); - } -} diff --git a/app/src/main/java/org/tasks/etesync/EteSyncClient.kt b/app/src/main/java/org/tasks/etesync/EteSyncClient.kt new file mode 100644 index 000000000..ffb4a8e6e --- /dev/null +++ b/app/src/main/java/org/tasks/etesync/EteSyncClient.kt @@ -0,0 +1,276 @@ +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 com.google.common.collect.Lists +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +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 +import javax.net.ssl.TrustManager + +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(null) + 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.add(builder) + } + httpClient = builder.build() + httpUrl = url?.toHttpUrlOrNull() + journalManager = JournalManager(httpClient, httpUrl!!) + } + + @Throws(NoSuchAlgorithmException::class, KeyManagementException::class) + fun forAccount(account: CaldavAccount): EteSyncClient { + return forUrl( + account.url, + account.username, + account.getEncryptionPassword(encryption), + account.getPassword(encryption)) + } + + @Throws(KeyManagementException::class, NoSuchAlgorithmException::class) + fun forUrl(url: String?, username: String?, encryptionPassword: String, token: String): EteSyncClient { + return EteSyncClient( + context, + encryption, + preferences, + interceptor, + url, + username, + encryptionPassword, + token, + foreground) + } + + @Throws(IOException::class, Exceptions.HttpException::class) + fun getToken(password: String?): String? { + return JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!) + } + + @get:Throws(Exceptions.HttpException::class) + val userInfo: UserInfoManager.UserInfo? + get() { + val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!) + return 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) + fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map { + val result: MutableMap = 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) + } + } + } + return result + } + + @Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class) + fun getSyncEntries( + userInfo: UserInfoManager.UserInfo?, + journal: Journal, + calendar: CaldavCalendar, + callback: (List>) -> Unit) { + val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!) + val crypto = getCrypto(userInfo, journal) + var journalEntries: List + 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) + } + + @Throws(Exceptions.HttpException::class) + fun pushEntries(journal: Journal, entries: List?, remoteCtag: String?) { + 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 + } + } + + fun setForeground() { + foreground = true + } + + fun invalidateToken() { + try { + JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!) + } catch (e: Exception) { + Timber.e(e) + } + } + + @Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class) + fun makeCollection(name: String?, color: Int): String { + 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)) + return uid + } + + @Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class) + fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? { + 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)) + return uid + } + + @Throws(Exceptions.HttpException::class) + fun deleteCollection(calendar: CaldavCalendar) { + journalManager!!.delete(Journal.fakeWithUid(calendar.url!!)) + } + + @Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class) + fun createUserInfo(derivedKey: String?) { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etesync/EteSynchronizer.java b/app/src/main/java/org/tasks/etesync/EteSynchronizer.java deleted file mode 100644 index bf5d24393..000000000 --- a/app/src/main/java/org/tasks/etesync/EteSynchronizer.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.tasks.etesync; - -import static com.google.common.collect.FluentIterable.from; -import static com.google.common.collect.Maps.newHashMap; -import static com.google.common.collect.Sets.newHashSet; -import static java.util.Collections.emptySet; -import static org.tasks.Strings.isNullOrEmpty; - -import android.content.Context; -import androidx.core.util.Pair; -import at.bitfire.ical4android.ICalendar; -import com.etesync.journalmanager.Crypto.CryptoManager; -import com.etesync.journalmanager.Exceptions; -import com.etesync.journalmanager.Exceptions.HttpException; -import com.etesync.journalmanager.Exceptions.IntegrityException; -import com.etesync.journalmanager.Exceptions.VersionTooNewException; -import com.etesync.journalmanager.JournalEntryManager; -import com.etesync.journalmanager.JournalEntryManager.Entry; -import com.etesync.journalmanager.JournalManager.Journal; -import com.etesync.journalmanager.UserInfoManager.UserInfo; -import com.etesync.journalmanager.model.CollectionInfo; -import com.etesync.journalmanager.model.SyncEntry; -import com.etesync.journalmanager.model.SyncEntry.Actions; -import com.google.common.collect.Iterables; -import com.todoroo.astrid.helper.UUIDHelper; -import com.todoroo.astrid.service.TaskDeleter; -import dagger.hilt.android.qualifiers.ApplicationContext; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.inject.Inject; -import net.fortuna.ical4j.model.property.ProdId; -import org.tasks.BuildConfig; -import org.tasks.LocalBroadcastManager; -import org.tasks.R; -import org.tasks.billing.Inventory; -import org.tasks.caldav.iCalendar; -import org.tasks.data.CaldavAccount; -import org.tasks.data.CaldavCalendar; -import org.tasks.data.CaldavDaoBlocking; -import org.tasks.data.CaldavTask; -import org.tasks.data.CaldavTaskContainer; -import timber.log.Timber; - -public class EteSynchronizer { - - static { - ICalendar.Companion.setProdId( - new ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")); - } - - private final CaldavDaoBlocking caldavDao; - private final LocalBroadcastManager localBroadcastManager; - private final TaskDeleter taskDeleter; - private final Inventory inventory; - private final EteSyncClient client; - private final iCalendar iCal; - private final Context context; - - @Inject - public EteSynchronizer( - @ApplicationContext Context context, - CaldavDaoBlocking caldavDao, - LocalBroadcastManager localBroadcastManager, - TaskDeleter taskDeleter, - Inventory inventory, - EteSyncClient client, - iCalendar iCal) { - this.context = context; - this.caldavDao = caldavDao; - this.localBroadcastManager = localBroadcastManager; - this.taskDeleter = taskDeleter; - this.inventory = inventory; - this.client = client; - this.iCal = iCal; - } - - public void sync(CaldavAccount account) { - if (!inventory.hasPro()) { - setError(account, context.getString(R.string.requires_pro_subscription)); - return; - } - if (isNullOrEmpty(account.getPassword())) { - setError(account, context.getString(R.string.password_required)); - return; - } - if (isNullOrEmpty(account.getEncryptionKey())) { - setError(account, context.getString(R.string.encryption_password_required)); - return; - } - try { - synchronize(account); - } catch (KeyManagementException - | NoSuchAlgorithmException - | HttpException - | IntegrityException - | VersionTooNewException e) { - setError(account, e.getMessage()); - } - } - - private void synchronize(CaldavAccount account) - throws KeyManagementException, NoSuchAlgorithmException, Exceptions.HttpException, - IntegrityException, VersionTooNewException { - EteSyncClient client = this.client.forAccount(account); - UserInfo userInfo = client.getUserInfo(); - Map resources = client.getCalendars(userInfo); - - Set uids = newHashSet(Iterables.transform(resources.values(), CollectionInfo::getUid)); - Timber.d("Found uids: %s", uids); - for (CaldavCalendar calendar : - caldavDao.findDeletedCalendars(account.getUuid(), new ArrayList<>(uids))) { - taskDeleter.delete(calendar); - } - - for (Map.Entry entry : resources.entrySet()) { - CollectionInfo collection = entry.getValue(); - String uid = collection.getUid(); - - CaldavCalendar calendar = caldavDao.getCalendarByUrl(account.getUuid(), uid); - Integer colorInt = collection.getColor(); - int color = colorInt == null ? 0 : colorInt; - if (calendar == null) { - calendar = new CaldavCalendar(); - calendar.setName(collection.getDisplayName()); - calendar.setAccount(account.getUuid()); - calendar.setUrl(collection.getUid()); - calendar.setUuid(UUIDHelper.newUUID()); - calendar.setColor(color); - caldavDao.insert(calendar); - } else { - if (!calendar.getName().equals(collection.getDisplayName()) - || calendar.getColor() != color) { - calendar.setName(collection.getDisplayName()); - calendar.setColor(color); - caldavDao.update(calendar); - localBroadcastManager.broadcastRefreshList(); - } - } - sync(client, userInfo, calendar, entry.getKey()); - } - setError(account, ""); - } - - private void setError(CaldavAccount account, String message) { - account.setError(message); - caldavDao.update(account); - localBroadcastManager.broadcastRefreshList(); - if (!isNullOrEmpty(message)) { - Timber.e(message); - } - } - - private void sync( - EteSyncClient client, UserInfo userInfo, CaldavCalendar caldavCalendar, Journal journal) - throws IntegrityException, Exceptions.HttpException, VersionTooNewException { - Timber.d("sync(%s)", caldavCalendar); - - Map localChanges = newHashMap(); - for (CaldavTaskContainer task : caldavDao.getCaldavTasksToPush(caldavCalendar.getUuid())) { - localChanges.put(task.getRemoteId(), task); - } - - String remoteCtag = journal.getLastUid(); - if (isNullOrEmpty(remoteCtag) || !remoteCtag.equals(caldavCalendar.getCtag())) { - Timber.v("Applying remote changes"); - client.getSyncEntries( - userInfo, - journal, - caldavCalendar, - syncEntries -> applyEntries(caldavCalendar, syncEntries, localChanges.keySet())); - } else { - Timber.d("%s up to date", caldavCalendar.getName()); - } - - List changes = new ArrayList<>(); - for (CaldavTask task : caldavDao.getDeleted(caldavCalendar.getUuid())) { - String vtodo = task.getVtodo(); - if (!isNullOrEmpty(vtodo)) { - changes.add(new SyncEntry(vtodo, Actions.DELETE)); - } - } - - for (CaldavTaskContainer task : localChanges.values()) { - String vtodo = task.getVtodo(); - boolean existingTask = !isNullOrEmpty(vtodo); - - if (task.isDeleted()) { - if (existingTask) { - changes.add(new SyncEntry(vtodo, Actions.DELETE)); - } - } else { - changes.add( - new SyncEntry( - new String(iCal.toVtodo(task.getCaldavTask(), task.getTask())), - existingTask ? Actions.CHANGE : Actions.ADD)); - } - } - - remoteCtag = caldavCalendar.getCtag(); - CryptoManager crypto = client.getCrypto(userInfo, journal); - List> updates = new ArrayList<>(); - JournalEntryManager.Entry previous = - isNullOrEmpty(remoteCtag) ? null : Entry.getFakeWithUid(remoteCtag); - - for (SyncEntry syncEntry : changes) { - Entry entry = new 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, from(updates).transform(p -> p.first).toList(), remoteCtag); - Timber.v("Applying local changes"); - applyEntries(caldavCalendar, updates, emptySet()); - } - - Timber.d("UPDATE %s", caldavCalendar); - - caldavDao.update(caldavCalendar); - caldavDao.updateParents(caldavCalendar.getUuid()); - localBroadcastManager.broadcastRefresh(); - } - - private void applyEntries( - CaldavCalendar caldavCalendar, List> syncEntries, Set dirty) { - for (Pair entry : syncEntries) { - Entry journalEntry = entry.first; - SyncEntry syncEntry = entry.second; - Actions action = syncEntry.getAction(); - String vtodo = syncEntry.getContent(); - Timber.v("%s: %s", action, vtodo); - at.bitfire.ical4android.Task task = iCalendar.Companion.fromVtodo(vtodo); - if (task == null) { - continue; - } - String remoteId = task.getUid(); - CaldavTask caldavTask = caldavDao.getTaskByRemoteId(caldavCalendar.getUuid(), remoteId); - switch (action) { - case ADD: - case CHANGE: - if (dirty.contains(remoteId)) { - caldavTask.setVtodo(vtodo); - caldavDao.update(caldavTask); - } else { - iCal.fromVtodo(caldavCalendar, caldavTask, task, vtodo, null, null); - } - break; - case DELETE: - dirty.remove(remoteId); - if (caldavTask != null) { - if (caldavTask.isDeleted()) { - caldavDao.delete(caldavTask); - } else { - taskDeleter.delete(caldavTask.getTask()); - } - } - break; - } - caldavCalendar.setCtag(journalEntry.getUid()); - caldavDao.update(caldavCalendar); - } - } -} diff --git a/app/src/main/java/org/tasks/etesync/EteSynchronizer.kt b/app/src/main/java/org/tasks/etesync/EteSynchronizer.kt new file mode 100644 index 000000000..510d46367 --- /dev/null +++ b/app/src/main/java/org/tasks/etesync/EteSynchronizer.kt @@ -0,0 +1,221 @@ +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.CaldavDaoBlocking +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 + +class EteSynchronizer @Inject constructor( + @param:ApplicationContext private val context: Context, + private val caldavDao: CaldavDaoBlocking, + 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") + } + } + + fun sync(account: CaldavAccount) { + 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 fun synchronize(account: CaldavAccount) { + val client = client.forAccount(account) + val userInfo = client.userInfo + val resources = client.getCalendars(userInfo) + val uids: Set = 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 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 fun sync( + client: EteSyncClient, userInfo: UserInfoManager.UserInfo, caldavCalendar: CaldavCalendar, journal: Journal) { + Timber.d("sync(%s)", caldavCalendar) + val localChanges = HashMap() + 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 + ) { syncEntries: List> -> applyEntries(caldavCalendar, syncEntries, localChanges.keys) } + } else { + Timber.d("%s up to date", caldavCalendar.name) + } + val changes: MutableList = ArrayList() + for (task in caldavDao.getDeleted(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> = 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.map { 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 fun applyEntries( + caldavCalendar: CaldavCalendar, + syncEntries: List>, + dirty: MutableSet) { + 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) + } + } +} \ No newline at end of file