From f344bd3c28ac3f4da98723f7716387ae25d4ca61 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Thu, 15 Oct 2015 00:49:15 +0200 Subject: [PATCH] Tasks with new sync logic --- app/build.gradle | 5 +- .../davdroid/resource/LocalCalendar.java | 12 +- .../bitfire/davdroid/resource/LocalTask.java | 139 ++++++++++++ .../davdroid/resource/LocalTaskList.java | 146 ++++++++++++ .../syncadapter/CalendarSyncManager.java | 19 +- .../CalendarsSyncAdapterService.java | 4 +- .../syncadapter/ContactsSyncManager.java | 8 +- .../davdroid/syncadapter/SyncManager.java | 19 +- .../syncadapter/TasksSyncAdapterService.java | 26 ++- .../syncadapter/TasksSyncManager.java | 209 ++++++++++++++++++ .../davdroid/ui/settings/AccountFragment.java | 9 +- .../ui/setup/AccountDetailsFragment.java | 16 +- .../ui/setup/QueryServerDialogFragment.java | 8 +- .../ui/setup/SelectCollectionsAdapter.java | 3 +- dav4android | 2 +- ical4android | 2 +- vcard4android | 2 +- 17 files changed, 577 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java diff --git a/app/build.gradle b/app/build.gradle index fa6a60b1..d878ad40 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,15 +11,14 @@ apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion '23.0.1' - useLibrary 'org.apache.http.legacy' defaultConfig { applicationId "at.bitfire.davdroid" minSdkVersion 14 targetSdkVersion 23 - versionCode 74 - versionName "0.9-alpha2" + versionCode 75 + versionName "0.9-alpha3" buildConfigField "java.util.Date", "buildTime", "new java.util.Date()" } diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java index c708a0b0..c2bb046c 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java @@ -22,9 +22,13 @@ import android.provider.CalendarContract; import android.provider.CalendarContract.Calendars; import android.provider.CalendarContract.Events; import android.provider.CalendarContract.Reminders; +import android.text.TextUtils; import com.google.common.base.Joiner; +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.model.component.VTimeZone; + import java.io.FileNotFoundException; import java.util.LinkedList; import java.util.List; @@ -34,6 +38,7 @@ import at.bitfire.ical4android.AndroidCalendar; import at.bitfire.ical4android.AndroidCalendarFactory; import at.bitfire.ical4android.BatchOperation; import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.DateUtils; import at.bitfire.vcard4android.ContactsStorageException; import lombok.Cleanup; @@ -84,9 +89,10 @@ public class LocalCalendar extends AndroidCalendar implements LocalCollection { values.put(Calendars.OWNER_ACCOUNT, account.name); values.put(Calendars.SYNC_EVENTS, 1); values.put(Calendars.VISIBLE, 1); - if (info.timezone != null) { - // TODO parse VTIMEZONE - // values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.timezone)); + if (!TextUtils.isEmpty(info.timezone)) { + VTimeZone timeZone = DateUtils.parseVTimeZone(info.timezone); + if (timeZone != null && timeZone.getTimeZoneId() != null) + values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.getTimeZoneId().getValue())); } values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); if (Build.VERSION.SDK_INT >= 15) { diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java new file mode 100644 index 00000000..6e7b9ecc --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalTask.java @@ -0,0 +1,139 @@ +/* + * 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 at.bitfire.davdroid.resource; + +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.os.RemoteException; +import android.provider.CalendarContract.Events; + +import net.fortuna.ical4j.model.property.ProdId; + +import org.dmfs.provider.tasks.TaskContract.Tasks; + +import java.io.FileNotFoundException; +import java.text.ParseException; + +import at.bitfire.davdroid.BuildConfig; +import at.bitfire.ical4android.AndroidTask; +import at.bitfire.ical4android.AndroidTaskFactory; +import at.bitfire.ical4android.AndroidTaskList; +import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.Task; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +public class LocalTask extends AndroidTask implements LocalResource { + static { + Task.prodId = new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4android ical4j/2.x"); + } + + static final String COLUMN_ETAG = Tasks.SYNC1, + COLUMN_UID = Tasks._UID, + COLUMN_SEQUENCE = Tasks.SYNC2; + + @Getter protected String fileName; + @Getter @Setter protected String eTag; + + public LocalTask(@NonNull AndroidTaskList taskList, Task task, String fileName, String eTag) { + super(taskList, task); + this.fileName = fileName; + this.eTag = eTag; + } + + protected LocalTask(@NonNull AndroidTaskList taskList, long id, ContentValues baseInfo) { + super(taskList, id); + if (baseInfo != null) { + fileName = baseInfo.getAsString(Events._SYNC_ID); + eTag = baseInfo.getAsString(COLUMN_ETAG); + } + } + + + /* process LocalTask-specific fields */ + + @Override + protected void populateTask(ContentValues values) throws FileNotFoundException, RemoteException, ParseException { + super.populateTask(values); + + fileName = values.getAsString(Events._SYNC_ID); + eTag = values.getAsString(COLUMN_ETAG); + task.uid = values.getAsString(COLUMN_UID); + + if (values.containsKey(COLUMN_SEQUENCE)) + task.sequence = values.getAsInteger(COLUMN_SEQUENCE); + } + + @Override + protected void buildTask(ContentProviderOperation.Builder builder, boolean update) { + super.buildTask(builder, update); + builder .withValue(Tasks._SYNC_ID, fileName) + .withValue(COLUMN_UID, task.uid) + .withValue(COLUMN_SEQUENCE, task.sequence) + .withValue(COLUMN_ETAG, eTag); + } + + + /* custom queries */ + + public void updateFileNameAndUID(String uid) throws CalendarStorageException { + try { + String newFileName = uid + ".ics"; + + ContentValues values = new ContentValues(2); + values.put(Tasks._SYNC_ID, newFileName); + values.put(COLUMN_UID, uid); + taskList.provider.client.update(taskSyncURI(), values, null, null); + + fileName = newFileName; + if (task != null) + task.uid = uid; + + } catch (RemoteException e) { + throw new CalendarStorageException("Couldn't update UID", e); + } + } + + @Override + public void clearDirty(String eTag) throws CalendarStorageException { + try { + ContentValues values = new ContentValues(2); + values.put(Tasks._DIRTY, 0); + values.put(COLUMN_ETAG, eTag); + if (task != null) + values.put(COLUMN_SEQUENCE, task.sequence); + taskList.provider.client.update(taskSyncURI(), values, null, null); + + this.eTag = eTag; + } catch (RemoteException e) { + throw new CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e); + } + } + + + static class Factory implements AndroidTaskFactory { + static final Factory INSTANCE = new Factory(); + + @Override + public LocalTask newInstance(AndroidTaskList taskList, long id, ContentValues baseInfo) { + return new LocalTask(taskList, id, baseInfo); + } + + @Override + public LocalTask newInstance(AndroidTaskList taskList, Task task) { + return new LocalTask(taskList, task, null, null); + } + + @Override + public LocalTask[] newArray(int size) { + return new LocalTask[size]; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java new file mode 100644 index 00000000..85796a60 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalTaskList.java @@ -0,0 +1,146 @@ +/* + * 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 at.bitfire.davdroid.resource; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import org.dmfs.provider.tasks.TaskContract.TaskLists; +import org.dmfs.provider.tasks.TaskContract.Tasks; + +import java.io.FileNotFoundException; + +import at.bitfire.ical4android.AndroidTaskList; +import at.bitfire.ical4android.AndroidTaskListFactory; +import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.TaskProvider; +import lombok.Cleanup; +import lombok.NonNull; + +public class LocalTaskList extends AndroidTaskList implements LocalCollection { + + public static final int defaultColor = 0xFFC3EA6E; // "DAVdroid green" + + public static final String COLUMN_CTAG = TaskLists.SYNC_VERSION; + + static String[] BASE_INFO_COLUMNS = new String[] { + Tasks._ID, + Tasks._SYNC_ID, + LocalTask.COLUMN_ETAG + }; + + private static Boolean tasksProviderAvailable; + + + @Override + protected String[] taskBaseInfoColumns() { + return BASE_INFO_COLUMNS; + } + + + protected LocalTaskList(Account account, TaskProvider provider, long id) { + super(account, provider, LocalTask.Factory.INSTANCE, id); + } + + public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws CalendarStorageException { + TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks); + if (provider == null) + throw new CalendarStorageException("Couldn't access OpenTasks provider"); + + ContentValues values = new ContentValues(); + values.put(TaskLists._SYNC_ID, info.getURL()); + values.put(TaskLists.LIST_NAME, info.getTitle()); + values.put(TaskLists.LIST_COLOR, info.color != null ? info.color : defaultColor); + values.put(TaskLists.OWNER, account.name); + values.put(TaskLists.SYNC_ENABLED, 1); + values.put(TaskLists.VISIBLE, 1); + + return create(account, provider, values); + } + + + @Override + public LocalTask[] getAll() throws CalendarStorageException { + return (LocalTask[])queryTasks(null, null); + } + + @Override + public LocalTask[] getDeleted() throws CalendarStorageException { + return (LocalTask[])queryTasks(Tasks._DELETED + "!=0", null); + } + + @Override + public LocalTask[] getWithoutFileName() throws CalendarStorageException { + return (LocalTask[])queryTasks(Tasks._SYNC_ID + " IS NULL", null); + } + + @Override + public LocalResource[] getDirty() throws CalendarStorageException, FileNotFoundException { + LocalTask[] tasks = (LocalTask[])queryTasks(Tasks._DIRTY + "!=0", null); + if (tasks != null) + for (LocalTask task : tasks) + task.getTask().sequence++; + return tasks; + } + + + @Override + public String getCTag() throws CalendarStorageException { + try { + @Cleanup Cursor cursor = provider.client.query(taskListSyncUri(), new String[] { COLUMN_CTAG }, null, null, null); + if (cursor != null && cursor.moveToNext()) + return cursor.getString(0); + } catch (RemoteException e) { + throw new CalendarStorageException("Couldn't read local (last known) CTag", e); + } + return null; + } + + @Override + public void setCTag(String cTag) throws CalendarStorageException { + try { + ContentValues values = new ContentValues(1); + values.put(COLUMN_CTAG, cTag); + provider.client.update(taskListSyncUri(), values, null, null); + } catch (RemoteException e) { + throw new CalendarStorageException("Couldn't write local (last known) CTag", e); + } + } + + + // helpers + + public static boolean tasksProviderAvailable(@NonNull ContentResolver resolver) { + if (tasksProviderAvailable != null) + return tasksProviderAvailable; + else { + TaskProvider provider = TaskProvider.acquire(resolver, TaskProvider.ProviderName.OpenTasks); + return tasksProviderAvailable = (provider != null); + } + } + + + public static class Factory implements AndroidTaskListFactory { + public static final Factory INSTANCE = new Factory(); + + @Override + public AndroidTaskList newInstance(Account account, TaskProvider provider, long id) { + return new LocalTaskList(account, provider, id); + } + + @Override + public AndroidTaskList[] newArray(int size) { + return new LocalTaskList[size]; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.java index ee03a686..f3793b33 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarSyncManager.java @@ -9,7 +9,6 @@ package at.bitfire.davdroid.syncadapter; import android.accounts.Account; -import android.content.ContentProviderClient; import android.content.ContentValues; import android.content.Context; import android.content.SyncResult; @@ -36,7 +35,6 @@ 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.AddressData; import at.bitfire.dav4android.property.CalendarColor; import at.bitfire.dav4android.property.CalendarData; import at.bitfire.dav4android.property.DisplayName; @@ -46,14 +44,11 @@ import at.bitfire.dav4android.property.GetETag; import at.bitfire.davdroid.ArrayUtils; import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.resource.LocalCalendar; -import at.bitfire.davdroid.resource.LocalContact; import at.bitfire.davdroid.resource.LocalEvent; import at.bitfire.davdroid.resource.LocalResource; -import at.bitfire.ical4android.AndroidHostInfo; import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.ical4android.Event; import at.bitfire.ical4android.InvalidCalendarException; -import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.ContactsStorageException; import lombok.Cleanup; @@ -63,20 +58,16 @@ public class CalendarSyncManager extends SyncManager { MAX_MULTIGET = 30, NOTIFICATION_ID = 2; - protected AndroidHostInfo hostInfo; - - public CalendarSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result, LocalCalendar calendar) { - super(NOTIFICATION_ID, context, account, extras, provider, result); + public CalendarSyncManager(Context context, Account account, Bundle extras, SyncResult result, LocalCalendar calendar) { + super(NOTIFICATION_ID, context, account, extras, result); localCollection = calendar; } @Override protected void prepare() { - Thread.currentThread().setContextClassLoader(context.getClassLoader()); - - hostInfo = new AndroidHostInfo(context.getContentResolver()); + Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j collectionURL = HttpUrl.parse(localCalendar().getName()); davCollection = new DavCalendar(httpClient, collectionURL); @@ -191,13 +182,13 @@ public class CalendarSyncManager extends SyncManager { private void processVEvent(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException { Event[] events; try { - events = Event.fromStream(stream, charset, hostInfo); + events = Event.fromStream(stream, charset); } catch (InvalidCalendarException e) { Constants.log.error("Received invalid iCalendar, ignoring"); return; } - if (events.length == 1) { + if (events != null && events.length == 1) { Event newData = events[0]; // delete local event, if it exists diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java index 2adf4714..91cb7b43 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -55,11 +55,11 @@ public class CalendarsSyncAdapterService extends Service { try { for (LocalCalendar calendar : (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) { Constants.log.info("Synchronizing calendar #" + calendar.getId() + ", URL: " + calendar.getName()); - CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, provider, syncResult, calendar); + CalendarSyncManager syncManager = new CalendarSyncManager(getContext(), account, extras, syncResult, calendar); syncManager.performSync(); } } catch (CalendarStorageException e) { - Constants.log.error("Couldn't get list of local calendars", e); + Constants.log.error("Couldn't enumerate local calendars", e); } Constants.log.info("Calendar sync complete"); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java index 2a827978..99480451 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java @@ -60,11 +60,13 @@ public class ContactsSyncManager extends SyncManager { MAX_MULTIGET = 10, NOTIFICATION_ID = 1; + final protected ContentProviderClient provider; protected boolean hasVCard4; public ContactsSyncManager(Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult result) { - super(NOTIFICATION_ID, context, account, extras, provider, result); + super(NOTIFICATION_ID, context, account, extras, result); + this.provider = provider; } @@ -175,10 +177,10 @@ public class ContactsSyncManager extends SyncManager { private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException { Contact contacts[] = Contact.fromStream(stream, charset, downloader); - if (contacts.length == 1) { + if (contacts != null && contacts.length == 1) { Contact newData = contacts[0]; - // delete local contact, if it exists + // update local contact, if it exists LocalContact localContact = (LocalContact)localResources.get(fileName); if (localContact != null) { Constants.log.info("Updating " + fileName + " in local address book"); diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java index 3e9d8c03..ccee003d 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -11,7 +11,6 @@ import android.accounts.Account; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -67,7 +66,6 @@ abstract public class SyncManager { protected final Context context; protected final Account account; protected final Bundle extras; - protected final ContentProviderClient provider; protected final SyncResult syncResult; protected final AccountSettings settings; @@ -92,11 +90,10 @@ abstract public class SyncManager { - public SyncManager(int notificationId, Context context, Account account, Bundle extras, ContentProviderClient provider, SyncResult syncResult) { + public SyncManager(int notificationId, Context context, Account account, Bundle extras, SyncResult syncResult) { this.context = context; this.account = account; this.extras = extras; - this.provider = provider; this.syncResult = syncResult; // get account settings and generate httpClient @@ -167,9 +164,14 @@ abstract public class SyncManager { } } - } catch(HttpException|DavException e) { - Constants.log.error("HTTP/DAV Exception during sync", e); - syncResult.stats.numParseExceptions++; + } catch(Exception e) { + if (e instanceof HttpException || e instanceof DavException) { + Constants.log.error("HTTP/DAV Exception during sync", e); + syncResult.stats.numParseExceptions++; + } else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) { + Constants.log.error("Couldn't access local storage", e); + syncResult.databaseError = true; + } Intent detailsIntent = new Intent(context, DebugInfoActivity.class); detailsIntent.putExtra(DebugInfoActivity.KEY_EXCEPTION, e); @@ -195,9 +197,6 @@ abstract public class SyncManager { } notificationManager.notify(account.name, notificationId, notification); - } catch(CalendarStorageException|ContactsStorageException e) { - Constants.log.error("Couldn't access local storage", e); - syncResult.databaseError = true; } } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java index 751dcba8..cc2352a4 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncAdapterService.java @@ -16,6 +16,14 @@ import android.content.Intent; import android.content.SyncResult; import android.os.Bundle; import android.os.IBinder; +import android.provider.CalendarContract; + +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalTaskList; +import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.TaskProvider; +import lombok.Cleanup; public class TasksSyncAdapterService extends Service { private static SyncAdapter syncAdapter; @@ -43,8 +51,24 @@ public class TasksSyncAdapterService extends Service { } @Override - public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient providerClient, SyncResult syncResult) { + Constants.log.info("Starting task sync (" + authority + ")"); + try { + @Cleanup TaskProvider provider = TaskProvider.acquire(getContext().getContentResolver(), TaskProvider.ProviderName.OpenTasks); + if (provider == null) + throw new CalendarStorageException("Couldn't access OpenTasks provider"); + + for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null)) { + Constants.log.info("Synchronizing task list #" + taskList.getId() + ", URL: " + taskList.getName()); + TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, extras, provider, syncResult, taskList); + syncManager.performSync(); + } + } catch (CalendarStorageException e) { + Constants.log.error("Couldn't enumerate local task lists", e); + } + + Constants.log.info("Calendar sync complete"); } } diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java new file mode 100644 index 00000000..80f40032 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java @@ -0,0 +1,209 @@ +/* + * 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 at.bitfire.davdroid.syncadapter; + +import android.accounts.Account; +import android.content.ContentValues; +import android.content.Context; +import android.content.SyncResult; +import android.os.Bundle; +import android.text.TextUtils; + +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.squareup.okhttp.HttpUrl; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.ResponseBody; + +import org.dmfs.provider.tasks.TaskContract.TaskLists; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +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.CalendarColor; +import at.bitfire.dav4android.property.CalendarData; +import at.bitfire.dav4android.property.DisplayName; +import at.bitfire.dav4android.property.GetCTag; +import at.bitfire.dav4android.property.GetContentType; +import at.bitfire.dav4android.property.GetETag; +import at.bitfire.davdroid.ArrayUtils; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalResource; +import at.bitfire.davdroid.resource.LocalTask; +import at.bitfire.davdroid.resource.LocalTaskList; +import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.InvalidCalendarException; +import at.bitfire.ical4android.Task; +import at.bitfire.ical4android.TaskProvider; +import lombok.Cleanup; + +public class TasksSyncManager extends SyncManager { + + protected static final int + MAX_MULTIGET = 30, + NOTIFICATION_ID = 3; + + final protected TaskProvider provider; + + + public TasksSyncManager(Context context, Account account, Bundle extras, TaskProvider provider, SyncResult result, LocalTaskList taskList) { + super(NOTIFICATION_ID, context, account, extras, result); + this.provider = provider; + localCollection = taskList; + } + + + @Override + protected void prepare() { + Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j + + collectionURL = HttpUrl.parse(localTaskList().getSyncId()); + davCollection = new DavCalendar(httpClient, collectionURL); + } + + @Override + protected void queryCapabilities() throws DavException, IOException, HttpException, CalendarStorageException { + davCollection.propfind(0, DisplayName.NAME, CalendarColor.NAME, GetCTag.NAME); + + // update name and color + DisplayName pDisplayName = (DisplayName)davCollection.properties.get(DisplayName.NAME); + String displayName = (pDisplayName != null && !TextUtils.isEmpty(pDisplayName.displayName)) ? + pDisplayName.displayName : collectionURL.toString(); + + CalendarColor pColor = (CalendarColor)davCollection.properties.get(CalendarColor.NAME); + int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor; + + ContentValues values = new ContentValues(2); + Constants.log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color)); + values.put(TaskLists.LIST_NAME, displayName); + values.put(TaskLists.LIST_COLOR, color); + localTaskList().update(values); + } + + @Override + protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException { + LocalTask local = (LocalTask)resource; + return RequestBody.create( + DavCalendar.MIME_ICALENDAR, + local.getTask().toStream().toByteArray() + ); + } + + @Override + protected void listRemote() throws IOException, HttpException, DavException { + // fetch list of remote VTODOs and build hash table to index file name + davCalendar().calendarQuery("VTODO"); + remoteResources = new HashMap<>(davCollection.members.size()); + for (DavResource vCard : davCollection.members) { + String fileName = vCard.fileName(); + Constants.log.debug("Found remote VTODO: " + fileName); + remoteResources.put(fileName, vCard); + } + } + + @Override + protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException { + Constants.log.info("Downloading " + toDownload.size() + " tasks (" + MAX_MULTIGET + " at once)"); + + // download new/updated iCalendars from server + for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) { + Constants.log.info("Downloading " + Joiner.on(" + ").join(bunch)); + + if (bunch.length == 1) { + // only one contact, use GET + DavResource remote = bunch[0]; + + ResponseBody body = remote.get("text/calendar"); + String eTag = ((GetETag)remote.properties.get(GetETag.NAME)).eTag; + + @Cleanup InputStream stream = body.byteStream(); + processVTodo(remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8)); + + } else { + // multiple contacts, use multi-get + List urls = new LinkedList<>(); + for (DavResource remote : bunch) + urls.add(remote.location); + davCalendar().multiget(urls.toArray(new HttpUrl[urls.size()])); + + // process multiget results + for (DavResource remote : davCollection.members) { + String eTag; + GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME); + if (getETag != null) + eTag = getETag.eTag; + else + throw new DavException("Received multi-get response without ETag"); + + Charset charset = Charsets.UTF_8; + GetContentType getContentType = (GetContentType)remote.properties.get(GetContentType.NAME); + if (getContentType != null && getContentType.type != null) { + MediaType type = MediaType.parse(getContentType.type); + if (type != null) + charset = type.charset(Charsets.UTF_8); + } + + CalendarData calendarData = (CalendarData)remote.properties.get(CalendarData.NAME); + if (calendarData == null || calendarData.iCalendar == null) + throw new DavException("Received multi-get response without address data"); + + @Cleanup InputStream stream = new ByteArrayInputStream(calendarData.iCalendar.getBytes()); + processVTodo(remote.fileName(), eTag, stream, charset); + } + } + } + } + + + // helpers + + private LocalTaskList localTaskList() { return ((LocalTaskList)localCollection); } + private DavCalendar davCalendar() { return (DavCalendar)davCollection; } + + private void processVTodo(String fileName, String eTag, InputStream stream, Charset charset) throws IOException, CalendarStorageException { + Task[] tasks = null; + try { + tasks = Task.fromStream(stream, charset); + } catch (InvalidCalendarException e) { + Constants.log.error("Received invalid iCalendar, ignoring"); + return; + } + + if (tasks != null && tasks.length == 1) { + Task newData = tasks[0]; + + // update local task, if it exists + LocalTask localTask = (LocalTask)localResources.get(fileName); + if (localTask != null) { + Constants.log.info("Updating " + fileName + " in local tasklist"); + localTask.setETag(eTag); + localTask.update(newData); + syncResult.stats.numUpdates++; + } else { + Constants.log.info("Adding " + fileName + " to local task list"); + localTask = new LocalTask(localTaskList(), newData, fileName, eTag); + localTask.add(); + syncResult.stats.numInserts++; + } + } else + Constants.log.error("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java index e332e6fe..33a72d6a 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/settings/AccountFragment.java @@ -21,6 +21,7 @@ import android.provider.ContactsContract; import at.bitfire.davdroid.R; import at.bitfire.davdroid.syncadapter.AccountSettings; +import at.bitfire.ical4android.TaskProvider; import ezvcard.VCardVersion; public class AccountFragment extends PreferenceFragment { @@ -119,8 +120,8 @@ public class AccountFragment extends PreferenceFragment { prefSyncCalendars.setSummary(R.string.settings_sync_summary_not_available); } - /*final ListPreference prefSyncTasks = (ListPreference)findPreference("sync_interval_tasks"); - final Long syncIntervalTasks = settings.getSyncInterval(LocalTaskList.TASKS_AUTHORITY); + 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) @@ -130,7 +131,7 @@ public class AccountFragment extends PreferenceFragment { prefSyncTasks.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - settings.setSyncInterval(LocalTaskList.TASKS_AUTHORITY, Long.parseLong((String) newValue)); + settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Long.parseLong((String) newValue)); readFromAccount(); return true; } @@ -138,7 +139,7 @@ public class AccountFragment extends PreferenceFragment { } else { prefSyncTasks.setEnabled(false); prefSyncTasks.setSummary(R.string.settings_sync_summary_not_available); - }*/ + } // category: address book final CheckBoxPreference prefVCard4 = (CheckBoxPreference) findPreference("vcard4_support"); diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java index 44e09cbd..18d7c8c4 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.java @@ -36,9 +36,11 @@ import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.R; import at.bitfire.davdroid.resource.LocalAddressBook; import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalTaskList; import at.bitfire.davdroid.resource.ServerInfo; import at.bitfire.davdroid.syncadapter.AccountSettings; import at.bitfire.ical4android.CalendarStorageException; +import at.bitfire.ical4android.TaskProvider; import at.bitfire.vcard4android.ContactsStorageException; import lombok.Cleanup; @@ -120,12 +122,16 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher { } }); - /*addSync(account, LocalTaskList.TASKS_AUTHORITY, serverInfo.getTaskLists(), new AddSyncCallback() { + addSync(account, TaskProvider.ProviderName.OpenTasks.authority, serverInfo.getTaskLists(), new AddSyncCallback() { @Override - public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) throws LocalStorageException { - LocalTaskList.create(account, getActivity().getContentResolver(), todoList); - } - });*/ + public void createLocalCollection(Account account, ServerInfo.ResourceInfo todoList) { + try { + LocalTaskList.create(account, getActivity().getContentResolver(), todoList); + } catch (CalendarStorageException e) { + Constants.log.error("Couldn't create local task list", e); + } + } + }); getActivity().finish(); } else diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java index d06e9e5d..b6104922 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/QueryServerDialogFragment.java @@ -26,8 +26,10 @@ import java.net.URISyntaxException; import at.bitfire.dav4android.exception.DavException; import at.bitfire.dav4android.exception.HttpException; +import at.bitfire.davdroid.Constants; import at.bitfire.davdroid.R; import at.bitfire.davdroid.resource.DavResourceFinder; +import at.bitfire.davdroid.resource.LocalTaskList; import at.bitfire.davdroid.resource.ServerInfo; public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks { @@ -68,7 +70,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC ((AddAccountActivity)getActivity()).serverInfo = serverInfo; Fragment nextFragment; - if (!serverInfo.getTaskLists().isEmpty() /*&& !LocalTaskList.isAvailable(getActivity())*/) + if (!serverInfo.getTaskLists().isEmpty() && !LocalTaskList.tasksProviderAvailable(getActivity().getContentResolver())) nextFragment = new InstallAppsFragment(); else nextFragment = new SelectCollectionsFragment(); @@ -119,10 +121,10 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC /*if (ExceptionUtils.indexOfType(e, CertPathValidatorException.class) != -1) serverInfo.setErrorMessage(getContext().getString(R.string.exception_cert_path_validation, e.getMessage()));*/ } catch (HttpException e) { - Log.e(TAG, "HTTP error while querying server info", e); + Constants.log.error("HTTP error while querying server info", e); serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage())); } catch (DavException e) { - Log.e(TAG, "DAV error while querying server info", e); + Constants.log.error("DAV error while querying server info", e); serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage())); } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java b/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java index 992e8239..88f01b78 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/SelectCollectionsAdapter.java @@ -18,6 +18,7 @@ import android.widget.CheckedTextView; import android.widget.ListAdapter; import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.LocalTaskList; import at.bitfire.davdroid.resource.ServerInfo; import lombok.Getter; @@ -171,7 +172,7 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter } // disable task list selection if there's no local task provider - if (viewType == TYPE_TASK_LISTS_ROW /*&& !LocalTaskList.isAvailable(context)*/) { + if (viewType == TYPE_TASK_LISTS_ROW && !LocalTaskList.tasksProviderAvailable(context.getContentResolver())) { final CheckedTextView check = (CheckedTextView)v; check.setEnabled(false); check.setOnClickListener(new View.OnClickListener() { diff --git a/dav4android b/dav4android index 7530deb4..6fc7f30b 160000 --- a/dav4android +++ b/dav4android @@ -1 +1 @@ -Subproject commit 7530deb497c7c0ee78a583e7371ef9bfc4458a2e +Subproject commit 6fc7f30b614ecb23b6fa66b6fd848fd6ddc4bafe diff --git a/ical4android b/ical4android index ea504f25..62428770 160000 --- a/ical4android +++ b/ical4android @@ -1 +1 @@ -Subproject commit ea504f2512ad5e9a85391797bdbdeb6c92871cdf +Subproject commit 624287708eb7f093d2ddf2012e2c2c124cd6e206 diff --git a/vcard4android b/vcard4android index 53c1695d..83ba3128 160000 --- a/vcard4android +++ b/vcard4android @@ -1 +1 @@ -Subproject commit 53c1695db02cc371369e05cb02a0f1e537ac9eec +Subproject commit 83ba3128913ba6e57d021ad003bf992100baae43