Add CalDAV sync adapter

pull/645/head
Alex Baker 6 years ago
parent bb0f99f35a
commit 7eb76be346

6
.gitmodules vendored

@ -0,0 +1,6 @@
[submodule "ical4android"]
path = ical4android
url = git@gitlab.com:tasks.org/ical4android.git
[submodule "dav4android"]
path = dav4android
url = git@gitlab.com:tasks.org/dav4android.git

@ -4,17 +4,6 @@ task wrapper(type: Wrapper) {
gradleVersion = '4.1'
}
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
repositories {
jcenter()
google()
@ -107,6 +96,9 @@ final ROOM_VERSION = '1.0.0'
final TESTING_SUPPORT_VERSION = '1.0.0'
dependencies {
compile project(':ical4android')
compile project(':dav4android')
annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
compile "com.google.dagger:dagger:${DAGGER_VERSION}"
@ -126,6 +118,7 @@ dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
debugCompile 'com.android.support:multidex:1.0.2'
compile 'com.squareup.okhttp3:okhttp:3.9.1'
compile 'com.google.code.gson:gson:2.8.2'
compile 'com.github.rey5137:material:1.2.4'
compile 'com.nononsenseapps:filepicker:4.1.0'

16
app/proguard.pro vendored

@ -27,3 +27,19 @@
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.concurrent.LazyInit
-dontwarn com.google.errorprone.annotations.ForOverride
# okhttp
-dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
# https://gitlab.com/bitfireAT/davdroid/blob/9fc3921b3293e19bd7be7bfc3f24d799ed2446bc/app/proguard-rules.txt
-dontwarn aQute.**
-dontwarn groovy.** # Groovy-based ContentBuilder not used
-dontwarn javax.cache.** # no JCache support in Android
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class org.threeten.bp.** { *; } # keep ThreeTen (for time zone processing)
-keep class at.bitfire.** { *; } # all DAVdroid code is required

@ -20,4 +20,6 @@ public interface ApplicationComponent {
BroadcastComponent plus(BroadcastModule module);
IntentServiceComponent plus(IntentServiceModule module);
SyncAdapterComponent plus(SyncAdapterModule syncAdapterModule);
}

@ -0,0 +1,10 @@
package org.tasks.injection;
import org.tasks.caldav.CalDAVSyncAdapter;
import dagger.Subcomponent;
@Subcomponent(modules = SyncAdapterModule.class)
public interface SyncAdapterComponent {
void inject(CalDAVSyncAdapter calDAVSyncAdapter);
}

@ -20,4 +20,6 @@ public interface ApplicationComponent {
BroadcastComponent plus(BroadcastModule module);
IntentServiceComponent plus(IntentServiceModule module);
SyncAdapterComponent plus(SyncAdapterModule syncAdapterModule);
}

@ -0,0 +1,10 @@
package org.tasks.injection;
import org.tasks.caldav.CalDAVSyncAdapter;
import dagger.Subcomponent;
@Subcomponent(modules = SyncAdapterModule.class)
public interface SyncAdapterComponent {
void inject(CalDAVSyncAdapter calDAVSyncAdapter);
}

@ -24,6 +24,7 @@ import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.preferences.ActivityPermissionRequestor;
import org.tasks.preferences.PermissionRequestor;
import org.tasks.sync.SyncAdapters;
import javax.inject.Inject;
@ -39,6 +40,7 @@ public class GtasksPreferences extends InjectingPreferenceActivity {
@Inject GtaskSyncAdapterHelper gtaskSyncAdapterHelper;
@Inject PlayServicesAvailability playServicesAvailability;
@Inject DialogBuilder dialogBuilder;
@Inject SyncAdapters syncAdapters;
@Inject GoogleTaskDao googleTaskDao;
@Override
@ -94,12 +96,12 @@ public class GtasksPreferences extends InjectingPreferenceActivity {
}
@Override
protected void onPostResume() {
super.onPostResume();
protected void onResume() {
super.onResume();
CheckBoxPreference backgroundSync = (CheckBoxPreference) findPreference(getString(R.string.gtask_background_sync));
backgroundSync.setChecked(gtaskSyncAdapterHelper.isSyncEnabled());
if (gtaskSyncAdapterHelper.isMasterSyncEnabled()) {
if (syncAdapters.isMasterSyncEnabled()) {
backgroundSync.setSummary(null);
} else {
backgroundSync.setSummary(R.string.master_sync_warning);

@ -17,9 +17,9 @@ import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.api.MoveRequest;
import org.tasks.analytics.Tracker;
import org.tasks.gtasks.GtaskSyncAdapterHelper;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.gtasks.GtaskSyncAdapterHelper;
import org.tasks.injection.ApplicationScope;
import java.io.IOException;

@ -191,7 +191,7 @@ public class GoogleTaskSyncAdapter extends InjectingAbstractThreadedSyncAdapter
}
private void pushLocalChanges() throws UserRecoverableAuthIOException {
List<Task> tasks = taskDao.getTasksToPush();
List<Task> tasks = taskDao.getGoogleTasksToPush();
for (Task task : tasks) {
try {
pushTask(task, gtasksInvoker);

@ -78,10 +78,6 @@ public class GtaskSyncAdapterHelper {
getAccount() != null;
}
public boolean isMasterSyncEnabled() {
return ContentResolver.getMasterSyncAutomatically();
}
public void enableSynchronization(boolean enabled) {
Account account = getAccount();
if (account != null) {

@ -1,5 +1,6 @@
package org.tasks.injection;
import org.tasks.caldav.CalDAVSyncAdapter;
import org.tasks.gtasks.GoogleTaskSyncAdapter;
import dagger.Subcomponent;
@ -7,4 +8,6 @@ import dagger.Subcomponent;
@Subcomponent(modules = SyncAdapterModule.class)
public interface SyncAdapterComponent {
void inject(GoogleTaskSyncAdapter googleTaskSyncAdapter);
void inject(CalDAVSyncAdapter calDAVSyncAdapter);
}

@ -4,21 +4,21 @@ import com.google.common.base.Strings;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import org.tasks.gtasks.GtaskSyncAdapterHelper;
import org.tasks.sync.SyncAdapters;
import javax.inject.Inject;
public class GoogleTaskPusher {
private final GtaskSyncAdapterHelper gtaskSyncAdapterHelper;
private final SyncAdapters syncAdapters;
@Inject
public GoogleTaskPusher(GtaskSyncAdapterHelper gtaskSyncAdapterHelper) {
this.gtaskSyncAdapterHelper = gtaskSyncAdapterHelper;
public GoogleTaskPusher(SyncAdapters syncAdapters) {
this.syncAdapters = syncAdapters;
}
void push(Task task, Task original) {
if(!gtaskSyncAdapterHelper.isEnabled()) {
if(!syncAdapters.isGoogleTaskSyncEnabled()) {
return;
}
@ -35,7 +35,7 @@ public class GoogleTaskPusher {
!task.getCompletionDate().equals(original.getCompletionDate()) ||
!task.getDeletionDate().equals(original.getDeletionDate()) ||
task.checkAndClearTransitory(SyncFlags.FORCE_SYNC)) {
gtaskSyncAdapterHelper.requestSynchronization();
syncAdapters.requestSynchronization();
}
}
}

@ -7,13 +7,16 @@ import javax.inject.Inject;
public class PushReceiver {
private final GoogleTaskPusher googleTaskPusher;
private final CalDAVPushReceiver calDAVPushReceiver;
@Inject
public PushReceiver(GoogleTaskPusher googleTaskPusher) {
public PushReceiver(GoogleTaskPusher googleTaskPusher, CalDAVPushReceiver calDAVPushReceiver) {
this.googleTaskPusher = googleTaskPusher;
this.calDAVPushReceiver = calDAVPushReceiver;
}
public void push(Task task, Task original) {
googleTaskPusher.push(task, original);
calDAVPushReceiver.push(task, original);
}
}

@ -523,6 +523,18 @@
</intent-filter>
</receiver>
<service
android:name=".caldav.CalDAVSyncService"
android:exported="true"
android:permission="signature">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_adapter_caldav" />
</service>
<service
android:name=".caldav.CalDAVAccountAuthenticatorService"
android:exported="false">

@ -44,8 +44,8 @@ public class DefaultsPreferences extends InjectingPreferenceActivity implements
@Inject CalendarProvider calendarProvider;
@Inject ActivityPermissionRequestor permissionRequester;
@Inject Tracker tracker;
@Inject SyncAdapters syncAdapters;
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject SyncAdapters syncAdapters;
private Preference defaultCalendarPref;

@ -97,7 +97,13 @@ public abstract class TaskDao {
"LEFT JOIN google_tasks ON tasks._id = google_tasks.task " +
"WHERE tasks.modified > google_tasks.last_sync " +
"OR google_tasks.remote_id = ''")
public abstract List<Task> getTasksToPush();
public abstract List<Task> getGoogleTasksToPush();
@android.arch.persistence.room.Query("SELECT tasks.* FROM tasks " +
"LEFT JOIN caldav_tasks ON tasks._id = caldav_tasks.task " +
"WHERE caldav_tasks.account = :uid " +
"AND tasks.modified > caldav_tasks.last_sync")
public abstract List<Task> getCaldavTasksToPush(String uid);
@android.arch.persistence.room.Query("SELECT * FROM TASKS " +
"WHERE completed = 0 AND deleted = 0 AND (notificationFlags > 0 OR notifications > 0)")

@ -623,6 +623,10 @@ public class Task implements Parcelable {
setDueDate(newDueDate);
}
public boolean isRecurring() {
return !Strings.isNullOrEmpty(recurrence);
}
public String getRecurrence() {
return recurrence;
}
@ -739,6 +743,10 @@ public class Task implements Parcelable {
modified = modificationDate;
}
public Long getModificationDate() {
return modified;
}
public Integer getReminderFlags() {
return notificationFlags;
}

@ -6,6 +6,7 @@ import com.todoroo.astrid.data.Task;
import org.tasks.calendars.CalendarEventProvider;
import org.tasks.data.AlarmDao;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.LocationDao;
import org.tasks.data.TagDao;
@ -28,17 +29,19 @@ public class TaskDeleter {
private final LocationDao locationDao;
private final TagDao tagDao;
private final GoogleTaskDao googleTaskDao;
private final CaldavDao caldavDao;
@Inject
public TaskDeleter(TaskDao taskDao, CalendarEventProvider calendarEventProvider,
AlarmDao alarmDao, LocationDao locationDao, TagDao tagDao,
GoogleTaskDao googleTaskDao) {
GoogleTaskDao googleTaskDao, CaldavDao caldavDao) {
this.taskDao = taskDao;
this.calendarEventProvider = calendarEventProvider;
this.alarmDao = alarmDao;
this.locationDao = locationDao;
this.tagDao = tagDao;
this.googleTaskDao = googleTaskDao;
this.caldavDao = caldavDao;
}
public int purgeDeleted() {
@ -51,6 +54,7 @@ public class TaskDeleter {
locationDao.deleteByTaskId(id);
tagDao.deleteByTaskId(id);
googleTaskDao.deleteByTaskId(id);
caldavDao.deleteById(id);
}
return deleted.size();
}

@ -0,0 +1,285 @@
package org.tasks.caldav;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import com.google.common.base.Strings;
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 org.apache.commons.codec.Charsets;
import org.tasks.AccountManager;
import org.tasks.LocalBroadcastManager;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.injection.InjectingAbstractThreadedSyncAdapter;
import org.tasks.injection.SyncAdapterComponent;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.List;
import javax.inject.Inject;
import at.bitfire.dav4android.BasicDigestAuthHandler;
import at.bitfire.dav4android.DavCalendar;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.InvalidCalendarException;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import timber.log.Timber;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class CalDAVSyncAdapter extends InjectingAbstractThreadedSyncAdapter {
@Inject AccountManager accountManager;
@Inject CaldavDao caldavDao;
@Inject CaldavAccountManager caldavAccountManager;
@Inject TaskDao taskDao;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject TaskCreator taskCreator;
CalDAVSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
String accountName = account.name;
String uuid = caldavAccountManager.getUuid(account);
Timber.d("onPerformSync: %s [%s]", accountName, uuid);
if (Strings.isNullOrEmpty(uuid)) {
caldavAccountManager.removeAccount(account);
return;
}
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
CaldavAccount caldavAccount = caldavDao.getAccount(uuid);
if (caldavAccount == null) {
caldavAccount = new CaldavAccount(accountName, uuid);
caldavAccount.setId(caldavDao.insert(caldavAccount));
localBroadcastManager.broadcastRefreshList();
}
org.tasks.caldav.Account localAccount = caldavAccountManager.getAccount(caldavAccount.getUuid());
if (isNullOrEmpty(localAccount.getPassword())) {
syncResult.stats.numAuthExceptions++;
return;
}
syncResult.stats.numAuthExceptions = 0;
BasicDigestAuthHandler basicDigestAuthHandler = new BasicDigestAuthHandler(null, caldavAccount.getUsername(), localAccount.getPassword());
OkHttpClient httpClient = new OkHttpClient().newBuilder()
.addNetworkInterceptor(basicDigestAuthHandler)
.authenticator(basicDigestAuthHandler)
.cookieJar(new MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(false)
.build();
URI uri = URI.create(caldavAccount.getUrl());
HttpUrl httpUrl = HttpUrl.get(uri);
DavCalendar davCalendar = new DavCalendar(httpClient, httpUrl);
try {
pushLocalChanges(caldavAccount, httpClient, httpUrl);
davCalendar.propfind(0, GetCTag.NAME);
String remoteCtag = davCalendar.getProperties().get(GetCTag.class).getCTag();
String localCtag = caldavAccount.getCtag();
if (localCtag != null && localCtag.equals(remoteCtag)) {
Timber.d("%s up to date", caldavAccount.getName());
return;
}
// fetch etags
// check for deleted tasks
// multiget updated tasks
davCalendar.calendarQuery("VTODO", null, null);
// fetch and apply remote changes
for (DavResource vCard : davCalendar.getMembers()) {
ResponseBody responseBody = vCard.get("text/calendar");
GetETag eTag = (GetETag) vCard.getProperties().get(GetETag.NAME);
if (eTag == null || isNullOrEmpty(eTag.getETag())) {
throw new DavException("Received CalDAV GET response without ETag for " + vCard.getLocation());
}
MediaType contentType = responseBody.contentType();
Charset charset = contentType == null ? Charsets.UTF_8 : contentType.charset(Charsets.UTF_8);
InputStream stream = responseBody.byteStream();
try {
processVTodo(vCard.fileName(), caldavAccount, eTag.getETag(), stream, charset);
} finally {
if (stream != null) {
stream.close();
}
}
}
caldavAccount.setCtag(remoteCtag);
caldavDao.update(caldavAccount);
} catch (IOException | HttpException | DavException | CalendarStorageException e) {
Timber.e(e.getMessage(), e);
}
localBroadcastManager.broadcastRefresh();
}
private void pushLocalChanges(CaldavAccount caldavAccount, OkHttpClient httpClient, HttpUrl httpUrl) {
List<Task> tasks = taskDao.getCaldavTasksToPush(caldavAccount.getUuid());
for (com.todoroo.astrid.data.Task task : tasks) {
try {
pushTask(task, caldavAccount, httpClient, httpUrl);
} catch (IOException e) {
Timber.e(e, e.getMessage());
}
}
}
private boolean deleteRemoteResource(OkHttpClient httpClient, HttpUrl httpUrl, CaldavTask caldavTask) {
try {
if (!Strings.isNullOrEmpty(caldavTask.getRemoteId())) {
DavResource remote = new DavResource(httpClient, httpUrl.newBuilder().addPathSegment(caldavTask.getRemoteId() + ".ics").build());
remote.delete(null);
}
} catch (HttpException e) {
if (e.getStatus() != 404) {
Timber.e(e, e.getMessage());
return false;
}
} catch (IOException e) {
Timber.e(e.getMessage(), e);
return false;
}
caldavDao.delete(caldavTask);
return true;
}
private void pushTask(Task task, CaldavAccount caldavAccount, OkHttpClient httpClient, HttpUrl httpUrl) throws IOException {
Timber.d("pushing %s", task);
List<CaldavTask> deleted = getDeleted(task.getId(), caldavAccount);
if (!deleted.isEmpty()) {
for (CaldavTask entry : deleted) {
deleteRemoteResource(httpClient, httpUrl, entry);
}
return;
}
CaldavTask caldavMetadata = caldavDao.getTask(task.getId());
if (caldavMetadata == null) {
return;
}
if (task.isDeleted()) {
if (deleteRemoteResource(httpClient, httpUrl, caldavMetadata)) {
caldavDao.delete(caldavMetadata);
}
return;
}
at.bitfire.ical4android.Task remoteModel = TaskConverter.toCaldav(task);
if (Strings.isNullOrEmpty(caldavMetadata.getRemoteId())) {
String caldavUid = UUIDHelper.newUUID();
caldavMetadata.setRemoteId(caldavUid);
remoteModel.setUid(caldavUid);
} else {
remoteModel.setUid(caldavMetadata.getRemoteId());
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
remoteModel.write(os);
RequestBody requestBody = RequestBody.create(
DavCalendar.MIME_ICALENDAR,
os.toByteArray());
try {
DavResource remote = new DavResource(httpClient, httpUrl.newBuilder().addPathSegment(caldavMetadata.getRemoteId() + ".ics").build());
remote.put(requestBody, null, false);
} catch (HttpException e) {
Timber.e(e.getMessage(), e);
return;
}
long modified = currentTimeMillis();
task.setModificationDate(modified);
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
taskDao.save(task);
caldavMetadata.setLastSync(modified);
caldavDao.update(caldavMetadata);
}
private List<CaldavTask> getDeleted(long taskId, CaldavAccount caldavAccount) {
return caldavDao.getDeleted(taskId, caldavAccount.getUuid());
}
private void processVTodo(String fileName, CaldavAccount caldavAccount, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException {
List<at.bitfire.ical4android.Task> tasks;
try {
InputStreamReader inputStreamReader = new InputStreamReader(stream, charset);
tasks = at.bitfire.ical4android.Task.fromReader(inputStreamReader);
inputStreamReader.close();
} catch (InvalidCalendarException e) {
Timber.e(e.getMessage(), e);
return;
}
if (tasks.size() == 1) {
at.bitfire.ical4android.Task remote = tasks.get(0);
Task task;
CaldavTask caldavTask = caldavDao.getTask(caldavAccount.getUuid(), remote.getUid());
if (caldavTask == null) {
task = taskCreator.createWithValues(null, "");
taskDao.createNew(task);
caldavTask = new CaldavTask(task.getId(), caldavAccount.getUuid());
caldavTask.setRemoteId(remote.getUid());
Timber.d("NEW %s", remote);
} else {
task = taskDao.fetch(caldavTask.getTask());
Timber.d("UPDATE %s", remote);
}
TaskConverter.apply(task, remote);
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
taskDao.save(task);
caldavTask.setLastSync(DateUtilities.now() + 1000L);
if (caldavTask.getId() == Task.NO_ID) {
caldavDao.insert(caldavTask);
} else {
caldavDao.update(caldavTask);
}
} else {
Timber.e("Received VCALENDAR with %s VTODOs; ignoring %s", tasks.size(), fileName);
}
}
@Override
protected void inject(SyncAdapterComponent component) {
component.inject(this);
}
}

@ -0,0 +1,34 @@
package org.tasks.caldav;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import timber.log.Timber;
public class CalDAVSyncService extends Service {
private static final Object lock = new Object();
private static CalDAVSyncAdapter syncAdapter = null;
@Override
public void onCreate() {
super.onCreate();
Timber.d("Service created");
synchronized (lock) {
if (syncAdapter == null) {
syncAdapter = new CalDAVSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
Timber.d("Service destroyed");
}
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
}

@ -0,0 +1,69 @@
/*
* Copyright © 2013 2016 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package org.tasks.caldav;
import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.keyvalue.MultiKey;
import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.collections4.map.MultiKeyMap;
import java.util.LinkedList;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
/**
* Primitive cookie store that stores cookies in a (volatile) hash map.
* Will be sufficient for session cookies.
*/
public class MemoryCookieStore implements CookieJar {
/**
* Stored cookies. The multi-key consists of three parts: name, domain, and path.
* This ensures that cookies can be overwritten. [RFC 6265 5.3 Storage Model]
* Not thread-safe!
*/
protected final MultiKeyMap<String, Cookie> storage = MultiKeyMap.multiKeyMap(new HashedMap<MultiKey<? extends String>, Cookie>());
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
synchronized(storage) {
for (Cookie cookie : cookies)
storage.put(cookie.name(), cookie.domain(), cookie.path(), cookie);
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = new LinkedList<>();
synchronized(storage) {
MapIterator<MultiKey<? extends String>, Cookie> iter = storage.mapIterator();
while (iter.hasNext()) {
iter.next();
Cookie cookie = iter.getValue();
// remove expired cookies
if (cookie.expiresAt() <= System.currentTimeMillis()) {
iter.remove();
continue;
}
// add applicable cookies
if (cookie.matches(url))
cookies.add(cookie);
}
}
return cookies;
}
}

@ -0,0 +1,113 @@
package org.tasks.caldav;
import com.todoroo.astrid.data.Task;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.property.Completed;
import net.fortuna.ical4j.model.property.Due;
import net.fortuna.ical4j.model.property.RRule;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;
import timber.log.Timber;
import static com.todoroo.astrid.data.Task.DUE_DATE;
import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY;
import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY_TIME;
import static org.tasks.date.DateTimeUtils.newDateTime;
public class TaskConverter {
private static DateFormat DUE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd", Locale.US);
public static void apply(Task local, at.bitfire.ical4android.Task remote) {
if (remote.getCompletedAt() != null) {
local.setCompletionDate(remote.getCompletedAt().getDate().getTime());
}
local.setTitle(remote.getSummary());
local.setNotes(remote.getDescription());
local.setImportance(fromRemote(remote.getPriority()));
RRule repeatRule = remote.getRRule();
if (repeatRule != null) {
local.setRecurrence("RRULE:" + repeatRule.getValue() + (local.repeatAfterCompletion() ? ";FROM=COMPLETION" : ""));
}
Due due = remote.getDue();
if (due != null) {
Date dueDate = due.getDate();
if (dueDate instanceof DateTime) {
local.setDueDate(Task.createDueDate(URGENCY_SPECIFIC_DAY_TIME, dueDate.getTime()));
} else {
try {
local.setDueDate(Task.createDueDate(URGENCY_SPECIFIC_DAY, DUE_DATE_FORMAT.parse(due.getValue()).getTime()));
} catch (ParseException e) {
Timber.e(e, e.getMessage());
}
}
}
}
static int fromRemote(int remotePriority) {
switch (remotePriority) {
case 0:
return Task.IMPORTANCE_NONE;
case 1:
return Task.IMPORTANCE_DO_OR_DIE;
case 2:
return Task.IMPORTANCE_MUST_DO;
default:
return Task.IMPORTANCE_SHOULD_DO;
}
}
static int toRemote(int tasksPriority) {
switch (tasksPriority) {
case Task.IMPORTANCE_DO_OR_DIE:
return 1;
case Task.IMPORTANCE_MUST_DO:
return 2;
case Task.IMPORTANCE_SHOULD_DO:
return 3;
default:
return 0;
}
}
public static at.bitfire.ical4android.Task toCaldav(Task task) {
at.bitfire.ical4android.Task remote = new at.bitfire.ical4android.Task();
remote.setSummary(task.getTitle());
remote.setDescription(task.getNotes());
if (task.hasDueDate()) {
if (task.hasDueTime()) {
remote.setDue(new Due(new DateTime(task.getDueDate())));
} else {
try {
remote.setDue(new Due(newDateTime(task.getDueDate()).toString("yyyyMMdd")));
} catch (ParseException e) {
Timber.e(e, e.getMessage());
}
}
remote.setDue(new Due(task.hasDueTime()
? new DateTime(task.getDueDate())
: new Date(new org.tasks.time.DateTime(task.getDueDate()).toUTC().getMillis())));
}
if (task.isCompleted()) {
remote.setCompletedAt(new Completed(new DateTime(task.getCompletionDate())));
}
if (task.isRecurring()) {
try {
String rrule = task
.getRecurrenceWithoutFrom()
.replace("RRULE:", "");
remote.setRRule(new RRule(rrule));
} catch (ParseException e) {
Timber.e(e, e.getMessage());
}
}
remote.setPriority(toRemote(task.getImportance()));
return remote;
}
}

@ -41,8 +41,8 @@ public interface CaldavDao {
@Query("SELECT * FROM caldav_tasks WHERE task = :taskId AND deleted = 0 LIMIT 1")
CaldavTask getTask(long taskId);
@Query("SELECT * FROM caldav_tasks WHERE remote_id = :remoteId LIMIT 1")
CaldavTask getTask(String remoteId);
@Query("SELECT * FROM caldav_tasks WHERE account = :account AND remote_id = :remoteId LIMIT 1")
CaldavTask getTask(String account, String remoteId);
@Query("DELETE FROM caldav_tasks WHERE task = :taskId")
void deleteById(long taskId);

@ -102,4 +102,17 @@ public class CaldavTask {
public void setAccount(String account) {
this.account = account;
}
@Override
public String toString() {
return "CaldavTask{" +
"id=" + id +
", task=" + task +
", account='" + account + '\'' +
", remoteId='" + remoteId + '\'' +
", etag='" + etag + '\'' +
", lastSync=" + lastSync +
", deleted=" + deleted +
'}';
}
}

@ -151,6 +151,10 @@ public class Preferences {
return getStringValue(R.string.gcal_p_default);
}
public String getDefaultRemoteList() {
return getStringValue(R.string.p_default_remote_list);
}
public int getFirstDayOfWeek() {
int firstDayOfWeek = getIntegerFromString(R.string.p_start_of_week, 0);
return firstDayOfWeek < 1 || firstDayOfWeek > 7 ? 0 : firstDayOfWeek;

@ -0,0 +1,25 @@
package org.tasks.receivers;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import org.tasks.caldav.CaldavAccountManager;
import javax.inject.Inject;
public class CalDAVPushReceiver {
private final CaldavAccountManager caldavAccountManager;
@Inject
public CalDAVPushReceiver(CaldavAccountManager caldavAccountManager) {
this.caldavAccountManager = caldavAccountManager;
}
public void push(Task task, Task original) {
if(task.checkTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC)) {
return;
}
caldavAccountManager.requestSynchronization();
}
}

@ -0,0 +1,10 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}

@ -0,0 +1 @@
Subproject commit e00c8fa36990a1a6184fb6c8a7ab6229b3117d42

@ -0,0 +1 @@
Subproject commit 4c1ff076c12b577e7d8daaf35bcc6f09b80c5554

@ -1 +1 @@
include 'app'
include 'app', 'ical4android', 'dav4android'

Loading…
Cancel
Save