Add EteSynchronizer

pull/898/head
Alex Baker 6 years ago
parent 744a673c7b
commit 945c1d8412

@ -71,7 +71,11 @@ public class TaskDeleter {
}
public void delete(Task task) {
delete(ImmutableList.of(task.getId()));
delete(task.getId());
}
public void delete(Long task) {
delete(ImmutableList.of(task));
}
public void delete(List<Long> tasks) {

@ -104,7 +104,7 @@ public class CaldavConverter {
return remotePriority > 5 ? Math.min(9, remotePriority) : 9;
}
static at.bitfire.ical4android.Task toCaldav(CaldavTask caldavTask, Task task) {
public static at.bitfire.ical4android.Task toCaldav(CaldavTask caldavTask, Task task) {
at.bitfire.ical4android.Task remote = null;
try {
if (!Strings.isNullOrEmpty(caldavTask.getVtodo())) {

@ -84,6 +84,9 @@ public abstract class CaldavDao {
@Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object = :object LIMIT 1")
public abstract CaldavTask getTask(String calendar, String object);
@Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_remote_id = :remoteId")
public abstract CaldavTask getTaskByRemoteId(String calendar, String remoteId);
@Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId")
public abstract List<CaldavTask> getTasks(long taskId);
@ -96,6 +99,13 @@ public abstract class CaldavDao {
+ "WHERE cd_deleted = 0 AND cd_vtodo IS NOT NULL AND cd_vtodo != ''")
public abstract List<CaldavTaskContainer> getTasks();
@Query(
"SELECT task.*, caldav_task.* FROM tasks AS task "
+ "INNER JOIN caldav_tasks AS caldav_task ON _id = cd_task "
+ "WHERE cd_calendar = :calendar "
+ "AND modified > cd_last_sync")
public abstract List<CaldavTaskContainer> getCaldavTasksToPush(String calendar);
@Query("SELECT * FROM caldav_lists ORDER BY cdl_name COLLATE NOCASE")
public abstract List<CaldavCalendar> getCalendars();

@ -1,13 +1,30 @@
package org.tasks.data;
import androidx.room.Embedded;
import com.google.common.base.Strings;
import com.todoroo.astrid.data.Task;
public class CaldavTaskContainer {
@Embedded public Task task;
@Embedded public CaldavTask caldavTask;
public Task getTask() {
return task;
}
public CaldavTask getCaldavTask() {
return caldavTask;
}
public String getRemoteId() {
return caldavTask.getRemoteId();
}
public boolean isDeleted() {
return task.isDeleted();
}
public boolean isNew() {
return Strings.isNullOrEmpty(caldavTask.getVtodo());
}
}

@ -1,5 +1,7 @@
package org.tasks.etesync;
import static com.google.common.collect.Lists.transform;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
@ -8,19 +10,24 @@ import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier;
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 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;
@ -32,6 +39,7 @@ import okhttp3.OkHttpClient.Builder;
import okhttp3.internal.tls.OkHostnameVerifier;
import org.tasks.DebugNetworkInterceptor;
import org.tasks.caldav.MemoryCookieStore;
import org.tasks.data.CaldavAccount;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import org.tasks.security.Encryption;
@ -39,6 +47,8 @@ import timber.log.Timber;
public class EteSyncClient {
private static final int MAX_FETCH = 50;
private final Encryption encryption;
private final Preferences preferences;
private final DebugNetworkInterceptor interceptor;
@ -110,6 +120,15 @@ public class EteSyncClient {
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));
}
public EteSyncClient forUrl(String url, String username, String encryptionPassword, String token)
throws KeyManagementException, NoSuchAlgorithmException {
return new EteSyncClient(
@ -136,7 +155,7 @@ public class EteSyncClient {
return Pair.create(token, key);
}
public CryptoManager getCrypto(Journal journal)
CryptoManager getCrypto(Journal journal)
throws VersionTooNewException, IntegrityException {
return new CryptoManager(journal.getVersion(), encryptionPassword, journal.getUid());
}
@ -166,6 +185,19 @@ public class EteSyncClient {
return result;
}
List<Pair<Entry, SyncEntry>> getSyncEntries(Journal journal, @Nullable String ctag)
throws IntegrityException, Exceptions.HttpException, VersionTooNewException {
JournalEntryManager journalEntryManager =
new JournalEntryManager(httpClient, httpUrl, journal.getUid());
CryptoManager crypto = getCrypto(journal);
List<Entry> journalEntries = journalEntryManager.list(crypto, ctag, MAX_FETCH);
return transform(journalEntries, e -> Pair.create(e, SyncEntry.fromJournalEntry(crypto, e)));
}
void pushEntries(Journal journal, List<Entry> entries, String ctag) throws HttpException {
new JournalEntryManager(httpClient, httpUrl, journal.getUid()).create(entries, ctag);
}
public EteSyncClient setForeground() {
foreground = true;
return this;

@ -0,0 +1,306 @@
package org.tasks.etesync;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
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.caldav.CaldavUtils.getParent;
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.model.CollectionInfo;
import com.etesync.journalmanager.model.SyncEntry;
import com.etesync.journalmanager.model.SyncEntry.Actions;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.UUIDHelper;
import com.todoroo.astrid.service.TaskCreator;
import com.todoroo.astrid.service.TaskDeleter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.LinkedList;
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.CaldavConverter;
import org.tasks.caldav.CaldavUtils;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.data.CaldavTaskContainer;
import org.tasks.data.TagDao;
import org.tasks.data.TagData;
import org.tasks.data.TagDataDao;
import org.tasks.injection.ForApplication;
import timber.log.Timber;
public class EteSynchronizer {
static {
ICalendar.Companion.setProdId(
new ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN"));
}
private final CaldavDao caldavDao;
private final TaskDao taskDao;
private final TagDataDao tagDataDao;
private final TagDao tagDao;
private final LocalBroadcastManager localBroadcastManager;
private final TaskCreator taskCreator;
private final TaskDeleter taskDeleter;
private final Inventory inventory;
private final EteSyncClient client;
private final Context context;
@Inject
public EteSynchronizer(
@ForApplication Context context,
CaldavDao caldavDao,
TaskDao taskDao,
TagDataDao tagDataDao,
TagDao tagDao,
LocalBroadcastManager localBroadcastManager,
TaskCreator taskCreator,
TaskDeleter taskDeleter,
Inventory inventory,
EteSyncClient client) {
this.context = context;
this.caldavDao = caldavDao;
this.taskDao = taskDao;
this.tagDataDao = tagDataDao;
this.tagDao = tagDao;
this.localBroadcastManager = localBroadcastManager;
this.taskCreator = taskCreator;
this.taskDeleter = taskDeleter;
this.inventory = inventory;
this.client = client;
}
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
| IOException
| VersionTooNewException e) {
setError(account, e.getMessage());
}
}
private void synchronize(CaldavAccount account)
throws KeyManagementException, NoSuchAlgorithmException, Exceptions.HttpException,
IntegrityException, IOException, VersionTooNewException {
EteSyncClient client = this.client.forAccount(account);
Map<Journal, CollectionInfo> resources = client.getCalendars();
Set<String> uids = newHashSet(Iterables.transform(resources.values(), CollectionInfo::getUid));
Timber.d("Found uids: %s", uids);
for (CaldavCalendar calendar :
caldavDao.findDeletedCalendars(account.getUuid(), newArrayList(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);
if (calendar == null) {
calendar = new CaldavCalendar();
calendar.setName(collection.getDisplayName());
calendar.setAccount(account.getUuid());
calendar.setUrl(collection.getUid());
calendar.setUuid(UUIDHelper.newUUID());
caldavDao.insert(calendar);
} else {
// TODO: update db and broadcast list refresh if name changed
}
sync(client, calendar, entry.getKey());
}
setError(account, "");
}
private void setError(CaldavAccount account, String message) {
account.setError(message);
caldavDao.update(account);
localBroadcastManager.broadcastRefreshList();
if (!Strings.isNullOrEmpty(message)) {
Timber.e(message);
}
}
private void sync(EteSyncClient client, CaldavCalendar caldavCalendar, Journal journal)
throws IntegrityException, Exceptions.HttpException, IOException, VersionTooNewException {
Timber.d("sync(%s)", caldavCalendar);
Map<String, CaldavTaskContainer> localChanges = newHashMap();
for (CaldavTaskContainer task : caldavDao.getCaldavTasksToPush(caldavCalendar.getUuid())) {
localChanges.put(task.getRemoteId(), task);
}
List<Pair<Entry, SyncEntry>> syncEntries =
client.getSyncEntries(journal, caldavCalendar.getCtag());
applyEntries(caldavCalendar, syncEntries, localChanges.keySet());
List<SyncEntry> changes = new ArrayList<>();
for (CaldavTask task : caldavDao.getDeleted(caldavCalendar.getUuid())) {
changes.add(new SyncEntry(task.getVtodo(), Actions.DELETE));
}
for (CaldavTaskContainer task : localChanges.values()) {
changes.add(new SyncEntry(getVtodo(task), task.isNew() ? Actions.ADD : Actions.DELETE));
}
String remoteCtag = caldavCalendar.getCtag();
CryptoManager crypto = client.getCrypto(journal);
List<Pair<Entry, SyncEntry>> updates = new ArrayList<>();
JournalEntryManager.Entry previous =
Strings.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;
}
client.pushEntries(journal, from(updates).transform(p -> p.first).toList(), remoteCtag);
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 = CaldavUtils.fromVtodo(vtodo);
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 {
processVTodo(caldavCalendar, caldavTask, task, vtodo);
}
break;
case DELETE:
if (caldavTask != null) {
taskDeleter.delete(caldavTask.getTask());
}
break;
}
caldavCalendar.setCtag(journalEntry.getUid());
caldavDao.update(caldavCalendar);
}
}
private JournalEntryManager.Entry getSyncEntry(Entry previous, CaldavTaskContainer task) {
return null;
}
private String getVtodo(CaldavTaskContainer container) throws IOException {
Task task = container.getTask();
CaldavTask caldavTask = container.getCaldavTask();
at.bitfire.ical4android.Task remoteModel = CaldavConverter.toCaldav(caldavTask, task);
LinkedList<String> categories = remoteModel.getCategories();
categories.clear();
categories.addAll(transform(tagDataDao.getTagDataForTask(task.getId()), TagData::getName));
if (Strings.isNullOrEmpty(caldavTask.getRemoteId())) {
String caldavUid = UUIDHelper.newUUID();
caldavTask.setRemoteId(caldavUid);
remoteModel.setUid(caldavUid);
} else {
remoteModel.setUid(caldavTask.getRemoteId());
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
remoteModel.write(os);
return new String(os.toByteArray());
}
private void processVTodo(
CaldavCalendar calendar,
CaldavTask caldavTask,
at.bitfire.ical4android.Task remote,
String vtodo) {
Task task;
if (caldavTask == null) {
task = taskCreator.createWithValues("");
taskDao.createNew(task);
caldavTask = new CaldavTask(task.getId(), calendar.getUuid(), remote.getUid(), null);
} else {
task = taskDao.fetch(caldavTask.getTask());
}
CaldavConverter.apply(task, remote);
tagDao.applyTags(task, tagDataDao, CaldavUtils.getTags(tagDataDao, remote.getCategories()));
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
task.putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true);
taskDao.save(task);
caldavTask.setVtodo(vtodo);
caldavTask.setLastSync(DateUtilities.now() + 1000L);
caldavTask.setRemoteParent(getParent(remote));
if (caldavTask.getId() == Task.NO_ID) {
caldavTask.setId(caldavDao.insert(caldavTask));
Timber.d("NEW %s", caldavTask);
} else {
caldavDao.update(caldavTask);
Timber.d("UPDATE %s", caldavTask);
}
}
}

@ -17,6 +17,7 @@ import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.etesync.EteSynchronizer;
import org.tasks.gtasks.GoogleTaskSynchronizer;
import org.tasks.injection.InjectingWorker;
import org.tasks.injection.JobComponent;
@ -28,6 +29,7 @@ public class SyncWork extends InjectingWorker {
private static final Object LOCK = new Object();
@Inject CaldavSynchronizer caldavSynchronizer;
@Inject EteSynchronizer eteSynchronizer;
@Inject GoogleTaskSynchronizer googleTaskSynchronizer;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject Preferences preferences;
@ -69,7 +71,14 @@ public class SyncWork extends InjectingWorker {
ExecutorService executor = newFixedThreadPool(numThreads);
for (CaldavAccount account : caldavDao.getAccounts()) {
executor.execute(() -> caldavSynchronizer.sync(account));
executor.execute(
() -> {
if (account.isCaldavAccount()) {
caldavSynchronizer.sync(account);
} else if (account.isEteSyncAccount()) {
eteSynchronizer.sync(account);
}
});
}
List<GoogleTaskAccount> accounts = googleTaskListDao.getAccounts();
for (int i = 0; i < accounts.size(); i++) {

Loading…
Cancel
Save