diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 985eced2..0fb3cd24 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -106,6 +106,19 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars" />
+
+
+
+
+
+
+
0) ? e.retryAfter : Constants.DEFAULT_RETRY_DELAY;
+ } catch (Exception | OutOfMemoryError e) {
+ if (e instanceof CalendarStorageException || e instanceof SQLiteException) {
+ App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
+ syncResult.databaseError = true;
+ }
+
+ int syncPhase = R.string.sync_phase_journals;
+ String title = getContext().getString(R.string.sync_error_calendar, account.name);
+
+ notificationManager.setThrowable(e);
+
+ final Intent detailsIntent = notificationManager.getDetailsIntent();
+ detailsIntent.putExtra(KEY_ACCOUNT, account);
+ if (!(e instanceof Exceptions.UnauthorizedException)) {
+ detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority);
+ detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase);
+ }
+
+ notificationManager.notify(title, getContext().getString(syncPhase));
+ }
+
+ App.log.info("Task sync complete");
+ }
+
+ private void updateLocalTaskLists(TaskProvider provider, Account account, AccountSettings settings) throws CalendarStorageException {
+ EntityDataStore data = ((App) getContext().getApplicationContext()).getData();
+ ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.TASK_LIST);
+
+ Map remote = new HashMap<>();
+ List remoteJournals = JournalEntity.getJournals(data, service);
+ for (JournalEntity journalEntity : remoteJournals) {
+ remote.put(journalEntity.getUid(), journalEntity);
+ }
+
+ LocalTaskList[] local = (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null);
+
+ boolean updateColors = settings.getManageCalendarColors();
+
+ // delete obsolete local task lists
+ for (LocalTaskList list : local) {
+ String url = list.getSyncId();
+ if (!remote.containsKey(url)) {
+ App.log.fine("Deleting obsolete local task list" + url);
+ list.delete();
+ } else {
+ // remote CollectionInfo found for this local collection, update data
+ JournalEntity journalEntity = remote.get(url);
+ App.log.fine("Updating local task list " + url + " with " + journalEntity);
+ list.update(journalEntity, updateColors);
+ // we already have a local task list for this remote collection, don't take into consideration anymore
+ remote.remove(url);
+ }
+ }
+
+ // create new local task lists
+ for (String url : remote.keySet()) {
+ JournalEntity journalEntity = remote.get(url);
+ App.log.info("Adding local task list " + journalEntity);
+ LocalTaskList.create(account, provider, journalEntity);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.java
new file mode 100644
index 00000000..bdb795de
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright © 2013 – 2015 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 com.etesync.syncadapter.syncadapter;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+
+import com.etesync.syncadapter.AccountSettings;
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.Constants;
+import com.etesync.syncadapter.InvalidAccountException;
+import com.etesync.syncadapter.R;
+import com.etesync.syncadapter.journalmanager.Exceptions;
+import com.etesync.syncadapter.journalmanager.JournalEntryManager;
+import com.etesync.syncadapter.model.CollectionInfo;
+import com.etesync.syncadapter.model.SyncEntry;
+import com.etesync.syncadapter.resource.LocalResource;
+import com.etesync.syncadapter.resource.LocalTask;
+import com.etesync.syncadapter.resource.LocalTaskList;
+
+import org.apache.commons.codec.Charsets;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.logging.Level;
+
+import at.bitfire.ical4android.CalendarStorageException;
+import at.bitfire.ical4android.InvalidCalendarException;
+import at.bitfire.ical4android.Task;
+import at.bitfire.ical4android.TaskProvider;
+import at.bitfire.vcard4android.ContactsStorageException;
+import okhttp3.HttpUrl;
+
+public class TasksSyncManager extends SyncManager {
+ final private HttpUrl remote;
+
+ protected static final int MAX_MULTIGET = 30;
+
+ final protected TaskProvider provider;
+
+
+ public TasksSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, TaskProvider provider, SyncResult result, LocalTaskList taskList, HttpUrl remote) throws InvalidAccountException, Exceptions.IntegrityException, Exceptions.GenericCryptoException {
+ super(context, account, settings, extras, authority, result, taskList.getName(), CollectionInfo.Type.TASK_LIST, account.name);
+ this.provider = provider;
+ localCollection = taskList;
+ this.remote = remote;
+ }
+
+ @Override
+ protected int notificationId() {
+ return Constants.NOTIFICATION_TASK_SYNC;
+ }
+
+ @Override
+ protected String getSyncErrorTitle() {
+ return context.getString(R.string.sync_error_tasks, account.name);
+ }
+
+ @Override
+ protected String getSyncSuccessfullyTitle() {
+ return context.getString(R.string.sync_successfully_tasks, info.displayName,
+ account.name);
+ }
+
+ @Override
+ protected boolean prepare() throws CalendarStorageException, ContactsStorageException {
+ if (!super.prepare())
+ return false;
+
+ journal = new JournalEntryManager(httpClient, remote, localTaskList().getName());
+ return true;
+ }
+
+ @Override
+ protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
+ super.prepareDirty();
+ }
+
+ // helpers
+
+ private LocalTaskList localTaskList() { return ((LocalTaskList)localCollection); }
+
+ protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException, InvalidCalendarException {
+ InputStream is = new ByteArrayInputStream(cEntry.getContent().getBytes(Charsets.UTF_8));
+
+ Task[] tasks = Task.fromStream(is, Charsets.UTF_8);
+ if (tasks.length == 0) {
+ App.log.warning("Received VCard without data, ignoring");
+ return;
+ } else if (tasks.length > 1)
+ App.log.warning("Received multiple VCALs, using first one");
+
+ Task task = tasks[0];
+ LocalTask local = (LocalTask) localCollection.getByUid(task.uid);
+
+ if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
+ processTask(task, local);
+ } else {
+ App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
+ local.delete();
+ }
+ }
+
+ private LocalResource processTask(final Task newData, LocalTask localTask) throws IOException, ContactsStorageException, CalendarStorageException {
+ // delete local event, if it exists
+ if (localTask != null) {
+ App.log.info("Updating " + newData.uid + " in local calendar");
+ localTask.setETag(newData.uid);
+ localTask.update(newData);
+ syncResult.stats.numUpdates++;
+ } else {
+ App.log.info("Adding " + newData.uid + " to local calendar");
+ localTask = new LocalTask(localTaskList(), newData, newData.uid, newData.uid);
+ localTask.add();
+ syncResult.stats.numInserts++;
+ }
+
+ return localTask;
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java
index e8b82cf1..cc8935a3 100644
--- a/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java
+++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountSettingsActivity.java
@@ -34,6 +34,8 @@ import com.etesync.syncadapter.R;
import com.etesync.syncadapter.ui.setup.LoginCredentials;
import com.etesync.syncadapter.ui.setup.LoginCredentialsChangeFragment;
+import at.bitfire.ical4android.TaskProvider;
+
import static com.etesync.syncadapter.Constants.KEY_ACCOUNT;
public class AccountSettingsActivity extends BaseActivity {
@@ -150,6 +152,27 @@ public class AccountSettingsActivity extends BaseActivity {
prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available);
}
+ final ListPreference prefSyncTasks = (ListPreference)findPreference("sync_interval_tasks");
+ final Long syncIntervalTasks = settings.getSyncInterval(TaskProvider.ProviderName.OpenTasks.authority);
+ if (syncIntervalTasks != null) {
+ prefSyncTasks.setValue(syncIntervalTasks.toString());
+ if (syncIntervalTasks == AccountSettings.SYNC_INTERVAL_MANUALLY)
+ prefSyncTasks.setSummary(R.string.settings_sync_summary_manually);
+ else
+ prefSyncTasks.setSummary(getString(R.string.settings_sync_summary_periodically, prefSyncTasks.getEntry()));
+ prefSyncTasks.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Long.parseLong((String)newValue));
+ getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
+ return false;
+ }
+ });
+ } else {
+ prefSyncTasks.setEnabled(false);
+ prefSyncTasks.setSummary(R.string.settings_sync_summary_not_available);
+ }
+
final SwitchPreferenceCompat prefWifiOnly = (SwitchPreferenceCompat)findPreference("sync_wifi_only");
prefWifiOnly.setChecked(settings.getSyncWifiOnly());
prefWifiOnly.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a8a7b3a6..f88c5843 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -224,6 +224,7 @@
%s and immediately on local changes
Not available
Calendars sync. interval
+ Tasks sync. interval
- -1
- 300
@@ -291,6 +292,7 @@
Additional permissions required
Calendar sync failed (%s)
Contacts sync failed (%s)
+ Tasks sync failed (%s)
Error while %s
Integrity error while %s
Server error while %s
@@ -308,6 +310,7 @@
post processing
Authentication failed
User is inactive
+ Task list \"%s\" modified (%s)
Calendar \"%s\" modified (%s)
Contacts modified (%s)
%s modified.
diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml
index bc1c2047..b7c97a98 100644
--- a/app/src/main/res/xml/settings_account.xml
+++ b/app/src/main/res/xml/settings_account.xml
@@ -50,6 +50,13 @@
android:entries="@array/settings_sync_interval_names"
android:entryValues="@array/settings_sync_interval_seconds" />
+
+
+
+