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" /> + + + +