Convert EteSynchronizer to Kotlin

pull/1051/head
Alex Baker 4 years ago
parent 5658fca41f
commit 378580b1e8

@ -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<Journal, CollectionInfo> getCalendars(UserInfo userInfo)
throws Exceptions.HttpException {
Map<Journal, CollectionInfo> 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<List<Pair<Entry, SyncEntry>>> callback)
throws IntegrityException, Exceptions.HttpException, VersionTooNewException {
JournalEntryManager journalEntryManager =
new JournalEntryManager(httpClient, httpUrl, journal.getUid());
CryptoManager crypto = getCrypto(userInfo, journal);
List<Entry> 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<Entry> entries, String remoteCtag) throws HttpException {
JournalEntryManager journalEntryManager =
new JournalEntryManager(httpClient, httpUrl, journal.getUid());
for (List<Entry> 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);
}
}

@ -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<TrustManager>(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<Journal, CollectionInfo> {
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)
}
}
}
return result
}
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
fun getSyncEntries(
userInfo: UserInfoManager.UserInfo?,
journal: Journal,
calendar: CaldavCalendar,
callback: (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) {
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.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<JournalEntryManager.Entry>?, 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
}
}

@ -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<Journal, CollectionInfo> resources = client.getCalendars(userInfo);
Set<String> 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<Journal, CollectionInfo> 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<String, CaldavTaskContainer> 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<SyncEntry> 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<Pair<Entry, SyncEntry>> 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<Pair<Entry, SyncEntry>> syncEntries, Set<String> dirty) {
for (Pair<Entry, SyncEntry> 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);
}
}
}

@ -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<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 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<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
) { syncEntries: List<Pair<JournalEntryManager.Entry, SyncEntry>> -> applyEntries(caldavCalendar, syncEntries, localChanges.keys) }
} else {
Timber.d("%s up to date", caldavCalendar.name)
}
val changes: MutableList<SyncEntry> = 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<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.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<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)
}
}
}
Loading…
Cancel
Save