You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/syncadapter/TasksSyncManager.java

218 lines
8.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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.Context;
import android.content.SyncResult;
import android.os.Bundle;
import org.apache.commons.codec.Charsets;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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 java.util.logging.Level;
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.CalendarData;
import at.bitfire.dav4android.property.GetCTag;
import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
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;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
public class TasksSyncManager extends SyncManager {
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) throws InvalidAccountException {
super(context, account, settings, extras, authority, result, "taskList/" + taskList.getId());
this.provider = provider;
localCollection = taskList;
}
@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 void prepare() {
collectionURL = HttpUrl.parse(localTaskList().getSyncId());
davCollection = new DavCalendar(httpClient, collectionURL);
}
@Override
protected void queryCapabilities() throws DavException, IOException, HttpException {
davCollection.propfind(0, GetCTag.NAME);
}
@Override
protected RequestBody prepareUpload(LocalResource resource) throws IOException, CalendarStorageException {
LocalTask local = (LocalTask)resource;
App.log.log(Level.FINE, "Preparing upload of task " + local.getFileName(), local.getTask() );
ByteArrayOutputStream os = new ByteArrayOutputStream();
local.getTask().write(os);
return RequestBody.create(
DavCalendar.MIME_ICALENDAR,
os.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", null, null);
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
App.log.fine("Found remote VTODO: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
App.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)) {
if (Thread.interrupted())
return;
App.log.info("Downloading " + StringUtils.join(bunch, ", "));
if (bunch.length == 1) {
// only one contact, use GET
DavResource remote = bunch[0];
ResponseBody body = remote.get("text/calendar");
// CalDAV servers MUST return ETag on GET [https://tools.ietf.org/html/rfc4791#section-5.3.4]
GetETag eTag = (GetETag)remote.properties.get(GetETag.NAME);
if (eTag == null || StringUtils.isEmpty(eTag.eTag))
throw new DavException("Received CalDAV GET response without ETag for " + remote.location);
Charset charset = Charsets.UTF_8;
MediaType contentType = body.contentType();
if (contentType != null)
charset = contentType.charset(Charsets.UTF_8);
@Cleanup InputStream stream = body.byteStream();
processVTodo(remote.fileName(), eTag.eTag, stream, charset);
} else {
// multiple contacts, use multi-get
List<HttpUrl> 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;
try {
tasks = Task.fromStream(stream, charset);
} catch (InvalidCalendarException e) {
App.log.log(Level.SEVERE, "Received invalid iCalendar, ignoring", e);
return;
}
if (tasks.length == 1) {
Task newData = tasks[0];
// update local task, if it exists
LocalTask localTask = (LocalTask)localResources.get(fileName);
if (localTask != null) {
App.log.info("Updating " + fileName + " in local tasklist");
localTask.setETag(eTag);
localTask.update(newData);
syncResult.stats.numUpdates++;
} else {
App.log.info("Adding " + fileName + " to local task list");
localTask = new LocalTask(localTaskList(), newData, fileName, eTag);
localTask.add();
syncResult.stats.numInserts++;
}
} else
App.log.severe("Received VCALENDAR with not exactly one VTODO; ignoring " + fileName);
}
}