mirror of https://github.com/tasks/tasks
Convert EteSynchronizer to Kotlin
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…
Reference in New Issue