Suspending caldav/etesync network calls

pull/1052/head
Alex Baker 4 years ago
parent 39077910b7
commit 479c26c416

@ -3,16 +3,21 @@ package org.tasks.caldav
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.DebugNetworkInterceptor
import org.tasks.TestUtilities.newPreferences
import org.tasks.security.KeyStoreEncryption
@RunWith(AndroidJUnit4::class)
class CaldavClientTest {
@Test
fun dontCrashOnSpaceInUrl() {
val context = ApplicationProvider.getApplicationContext<Context>()
CaldavClient(context, null, newPreferences(context), null)
.forUrl("https://example.com/remote.php/a space/", "username", "password")
runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
CaldavClient(context, KeyStoreEncryption(), newPreferences(context), DebugNetworkInterceptor(ApplicationProvider.getApplicationContext()))
.forUrl("https://example.com/remote.php/a space/", "username", "password")
}
}
}

@ -6,6 +6,6 @@ import org.tasks.ui.CompletableViewModel
class AddCaldavAccountViewModel @ViewModelInject constructor(
private val client: CaldavClient) : CompletableViewModel<String>() {
suspend fun addAccount(url: String, username: String, password: String) {
run { client.setForeground().forUrl(url, username, password).homeSet }
run { client.setForeground().forUrl(url, username, password).homeSet() }
}
}

@ -226,7 +226,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
}
needsValidation() -> {
showProgressIndicator()
updateAccount(url, username, password)
updateAccount(url, username, password!!)
}
hasChanges() -> {
updateAccount()
@ -238,7 +238,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
}
protected abstract suspend fun addAccount(url: String, username: String, password: String)
protected abstract suspend fun updateAccount(url: String, username: String, password: String?)
protected abstract suspend fun updateAccount(url: String, username: String, password: String)
protected abstract suspend fun updateAccount()
protected abstract val helpUrl: String?

@ -56,7 +56,7 @@ class CaldavAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolb
override suspend fun addAccount(url: String, username: String, password: String) =
addCaldavAccountViewModel.addAccount(url, username, password)
override suspend fun updateAccount(url: String, username: String, password: String?) =
override suspend fun updateAccount(url: String, username: String, password: String) =
updateCaldavAccountViewModel.updateCaldavAccount(url, username, password)
override suspend fun updateAccount() =

@ -1,344 +0,0 @@
package org.tasks.caldav;
import static at.bitfire.dav4jvm.XmlUtils.NS_CALDAV;
import static at.bitfire.dav4jvm.XmlUtils.NS_CARDDAV;
import static at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV;
import static org.tasks.Strings.isNullOrEmpty;
import android.content.Context;
import at.bitfire.cert4android.CustomCertManager;
import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier;
import at.bitfire.dav4jvm.BasicDigestAuthHandler;
import at.bitfire.dav4jvm.DavResource;
import at.bitfire.dav4jvm.Property.Name;
import at.bitfire.dav4jvm.Response;
import at.bitfire.dav4jvm.Response.HrefRelation;
import at.bitfire.dav4jvm.XmlUtils;
import at.bitfire.dav4jvm.exception.DavException;
import at.bitfire.dav4jvm.exception.HttpException;
import at.bitfire.dav4jvm.property.CalendarColor;
import at.bitfire.dav4jvm.property.CalendarHomeSet;
import at.bitfire.dav4jvm.property.CurrentUserPrincipal;
import at.bitfire.dav4jvm.property.DisplayName;
import at.bitfire.dav4jvm.property.GetCTag;
import at.bitfire.dav4jvm.property.ResourceType;
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet;
import at.bitfire.dav4jvm.property.SyncToken;
import com.todoroo.astrid.helper.UUIDHelper;
import dagger.hilt.android.qualifiers.ApplicationContext;
import java.io.IOException;
import java.io.StringWriter;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.internal.tls.OkHostnameVerifier;
import org.tasks.DebugNetworkInterceptor;
import org.tasks.R;
import org.tasks.data.CaldavAccount;
import org.tasks.data.CaldavCalendar;
import org.tasks.preferences.Preferences;
import org.tasks.security.KeyStoreEncryption;
import org.tasks.ui.DisplayableException;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import timber.log.Timber;
public class CaldavClient {
private final KeyStoreEncryption encryption;
private final Preferences preferences;
private final DebugNetworkInterceptor interceptor;
private final OkHttpClient httpClient;
private final HttpUrl httpUrl;
private final Context context;
private final BasicDigestAuthHandler basicDigestAuthHandler;
private boolean foreground;
@Inject
CaldavClient(
@ApplicationContext Context context,
KeyStoreEncryption encryption,
Preferences preferences,
DebugNetworkInterceptor interceptor) {
this.context = context;
this.encryption = encryption;
this.preferences = preferences;
this.interceptor = interceptor;
httpClient = null;
httpUrl = null;
basicDigestAuthHandler = null;
}
private CaldavClient(
Context context,
KeyStoreEncryption encryption,
Preferences preferences,
DebugNetworkInterceptor interceptor,
String url,
String username,
String password,
boolean foreground)
throws NoSuchAlgorithmException, KeyManagementException {
this.context = context;
this.encryption = encryption;
this.preferences = preferences;
this.interceptor = interceptor;
CustomCertManager customCertManager = new CustomCertManager(context);
customCertManager.setAppInForeground(foreground);
CustomHostnameVerifier hostnameVerifier =
customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {customCertManager}, null);
basicDigestAuthHandler = new BasicDigestAuthHandler(null, username, password);
Builder builder =
new OkHttpClient()
.newBuilder()
.addNetworkInterceptor(basicDigestAuthHandler)
.authenticator(basicDigestAuthHandler)
.cookieJar(new MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.getSocketFactory(), customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS);
if (preferences.isFlipperEnabled()) {
interceptor.add(builder);
}
httpClient = builder.build();
httpUrl = HttpUrl.parse(url);
}
public CaldavClient forAccount(CaldavAccount account)
throws NoSuchAlgorithmException, KeyManagementException {
return forUrl(account.getUrl(), account.getUsername(), account.getPassword(encryption));
}
CaldavClient forCalendar(CaldavAccount account, CaldavCalendar calendar)
throws NoSuchAlgorithmException, KeyManagementException {
return forUrl(calendar.getUrl(), account.getUsername(), account.getPassword(encryption));
}
CaldavClient forUrl(String url, String username, String password)
throws KeyManagementException, NoSuchAlgorithmException {
return new CaldavClient(
context, encryption, preferences, interceptor, url, username, password, foreground);
}
private String tryFindPrincipal(String link) throws DavException, IOException {
HttpUrl url = httpUrl.resolve(link);
Timber.d("Checking for principal: %s", url);
DavResource davResource = new DavResource(httpClient, url);
ResponseList responses = new ResponseList();
davResource.propfind(0, new Name[] {CurrentUserPrincipal.NAME}, responses);
if (!responses.isEmpty()) {
Response response = responses.get(0);
CurrentUserPrincipal currentUserPrincipal = response.get(CurrentUserPrincipal.class);
if (currentUserPrincipal != null) {
String href = currentUserPrincipal.getHref();
if (!isNullOrEmpty(href)) {
return href;
}
}
}
return null;
}
private String findHomeset() throws DavException, IOException {
DavResource davResource = new DavResource(httpClient, httpUrl);
ResponseList responses = new ResponseList();
davResource.propfind(0, new Name[] {CalendarHomeSet.NAME}, responses);
Response response = responses.get(0);
CalendarHomeSet calendarHomeSet = response.get(CalendarHomeSet.class);
if (calendarHomeSet == null) {
throw new DisplayableException(R.string.caldav_home_set_not_found);
}
List<String> hrefs = calendarHomeSet.getHrefs();
if (hrefs.size() != 1) {
throw new DisplayableException(R.string.caldav_home_set_not_found);
}
String homeSet = hrefs.get(0);
if (isNullOrEmpty(homeSet)) {
throw new DisplayableException(R.string.caldav_home_set_not_found);
}
return davResource.getLocation().resolve(homeSet).toString();
}
String getHomeSet()
throws IOException, DavException, NoSuchAlgorithmException, KeyManagementException {
String principal = null;
try {
principal = tryFindPrincipal("/.well-known/caldav");
} catch (Exception e) {
if (e instanceof HttpException && ((HttpException) e).getCode() == 401) {
throw e;
}
Timber.w(e);
}
if (principal == null) {
principal = tryFindPrincipal("");
}
return forUrl(
(isNullOrEmpty(principal) ? this.httpUrl : httpUrl.resolve(principal)).toString(),
basicDigestAuthHandler.getUsername(),
basicDigestAuthHandler.getPassword())
.findHomeset();
}
public List<Response> getCalendars() throws IOException, DavException {
DavResource davResource = new DavResource(httpClient, httpUrl);
ResponseList responses = new ResponseList(HrefRelation.MEMBER);
davResource.propfind(
1,
new Name[] {
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME
},
responses);
List<Response> urls = new ArrayList<>();
for (Response member : responses) {
ResourceType resourceType = member.get(ResourceType.class);
if (resourceType == null
|| !resourceType.getTypes().contains(ResourceType.Companion.getCALENDAR())) {
Timber.d("%s is not a calendar", member);
continue;
}
SupportedCalendarComponentSet supportedCalendarComponentSet =
member.get(SupportedCalendarComponentSet.class);
if (supportedCalendarComponentSet == null
|| !supportedCalendarComponentSet.getSupportsTasks()) {
Timber.d("%s does not support tasks", member);
continue;
}
urls.add(member);
}
return urls;
}
void deleteCollection() throws IOException, HttpException {
new DavResource(httpClient, httpUrl).delete(null, response -> null);
}
String makeCollection(String displayName, int color)
throws IOException, XmlPullParserException, HttpException {
DavResource davResource =
new DavResource(httpClient, httpUrl.resolve(UUIDHelper.newUUID() + "/"));
String mkcolString = getMkcolString(displayName, color);
davResource.mkCol(mkcolString, response -> null);
return davResource.getLocation().toString();
}
String updateCollection(String displayName, int color)
throws IOException, XmlPullParserException, HttpException {
PatchableDavResource davResource = new PatchableDavResource(httpClient, httpUrl);
davResource.propPatch(getPropPatchString(displayName, color), response -> null);
return davResource.getLocation().toString();
}
private String getPropPatchString(String displayName, int color)
throws IOException, XmlPullParserException {
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
XmlSerializer xml = xmlPullParserFactory.newSerializer();
StringWriter stringWriter = new StringWriter();
xml.setOutput(stringWriter);
xml.startDocument("UTF-8", null);
xml.setPrefix("", NS_WEBDAV);
xml.setPrefix("CAL", NS_CALDAV);
xml.setPrefix("CARD", NS_CARDDAV);
xml.startTag(NS_WEBDAV, "propertyupdate");
xml.startTag(XmlUtils.NS_WEBDAV, "set");
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
setDisplayName(xml, displayName);
if (color != 0) {
setColor(xml, color);
}
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
xml.endTag(XmlUtils.NS_WEBDAV, "set");
if (color == 0) {
xml.startTag(XmlUtils.NS_WEBDAV, "remove");
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
xml.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
xml.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
xml.endTag(XmlUtils.NS_WEBDAV, "remove");
}
xml.endTag(XmlUtils.NS_WEBDAV, "propertyupdate");
xml.endDocument();
xml.flush();
return stringWriter.toString();
}
private String getMkcolString(String displayName, int color) throws IOException, XmlPullParserException {
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
XmlSerializer xml = xmlPullParserFactory.newSerializer();
StringWriter stringWriter = new StringWriter();
xml.setOutput(stringWriter);
xml.startDocument("UTF-8", null);
xml.setPrefix("", NS_WEBDAV);
xml.setPrefix("CAL", NS_CALDAV);
xml.setPrefix("CARD", NS_CARDDAV);
xml.startTag(NS_WEBDAV, "mkcol");
xml.startTag(XmlUtils.NS_WEBDAV, "set");
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
xml.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
xml.startTag(XmlUtils.NS_WEBDAV, "collection");
xml.endTag(XmlUtils.NS_WEBDAV, "collection");
xml.startTag(XmlUtils.NS_CALDAV, "calendar");
xml.endTag(XmlUtils.NS_CALDAV, "calendar");
xml.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
setDisplayName(xml, displayName);
if (color != 0) {
setColor(xml, color);
}
xml.startTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
xml.startTag(XmlUtils.NS_CALDAV, "comp");
xml.attribute(null, "name", "VTODO");
xml.endTag(XmlUtils.NS_CALDAV, "comp");
xml.endTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
xml.endTag(XmlUtils.NS_WEBDAV, "set");
xml.endTag(XmlUtils.NS_WEBDAV, "mkcol");
xml.endDocument();
xml.flush();
return stringWriter.toString();
}
private void setDisplayName(XmlSerializer xml, String name) throws IOException {
xml.startTag(XmlUtils.NS_WEBDAV, "displayname");
xml.text(name);
xml.endTag(XmlUtils.NS_WEBDAV, "displayname");
}
private void setColor(XmlSerializer xml, int color) throws IOException {
xml.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
xml.text(String.format("#%06X%02X", color & 0xFFFFFF, color >>> 24));
xml.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
}
OkHttpClient getHttpClient() {
return httpClient;
}
CaldavClient setForeground() {
foreground = true;
return this;
}
}

@ -0,0 +1,334 @@
package org.tasks.caldav
import android.content.Context
import androidx.annotation.WorkerThread
import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4jvm.BasicDigestAuthHandler
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation
import at.bitfire.dav4jvm.XmlUtils.NS_APPLE_ICAL
import at.bitfire.dav4jvm.XmlUtils.NS_CALDAV
import at.bitfire.dav4jvm.XmlUtils.NS_CARDDAV
import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.dav4jvm.property.ResourceType.Companion.CALENDAR
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import org.tasks.DebugNetworkInterceptor
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import org.tasks.ui.DisplayableException
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import org.xmlpull.v1.XmlSerializer
import timber.log.Timber
import java.io.IOException
import java.io.StringWriter
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
class CaldavClient {
private val encryption: KeyStoreEncryption
private val preferences: Preferences
private val interceptor: DebugNetworkInterceptor
val httpClient: OkHttpClient?
private val httpUrl: HttpUrl?
private val context: Context
private val basicDigestAuthHandler: BasicDigestAuthHandler?
private var foreground = false
@Inject
internal constructor(
@ApplicationContext context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
httpClient = null
httpUrl = null
basicDigestAuthHandler = null
}
private constructor(
context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor,
url: String?,
username: String,
password: String,
foreground: Boolean) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
val customCertManager = CustomCertManager(context)
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(null)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf<TrustManager>(customCertManager), null)
basicDigestAuthHandler = BasicDigestAuthHandler(null, username, password)
val builder = OkHttpClient()
.newBuilder()
.addNetworkInterceptor(basicDigestAuthHandler)
.authenticator(basicDigestAuthHandler)
.cookieJar(MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.socketFactory, customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
if (preferences.isFlipperEnabled) {
interceptor.add(builder)
}
httpClient = builder.build()
httpUrl = url?.toHttpUrlOrNull()
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): CaldavClient {
return forUrl(account.url, account.username!!, account.getPassword(encryption))
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forCalendar(account: CaldavAccount, calendar: CaldavCalendar): CaldavClient {
return forUrl(calendar.url, account.username!!, account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String?, username: String, password: String): CaldavClient = withContext(Dispatchers.IO) {
CaldavClient(
context, encryption, preferences, interceptor, url, username, password, foreground)
}
@WorkerThread
@Throws(DavException::class, IOException::class)
private fun tryFindPrincipal(link: String): String? {
val url = httpUrl!!.resolve(link)
Timber.d("Checking for principal: %s", url)
val davResource = DavResource(httpClient!!, url!!)
val responses = ArrayList<Response>()
davResource.propfind(0, CurrentUserPrincipal.NAME) { response, _ ->
responses.add(response)
}
if (responses.isNotEmpty()) {
val response = responses[0]
val currentUserPrincipal = response[CurrentUserPrincipal::class.java]
if (currentUserPrincipal != null) {
val href = currentUserPrincipal.href
if (!isNullOrEmpty(href)) {
return href
}
}
}
return null
}
@WorkerThread
@Throws(DavException::class, IOException::class)
private fun findHomeset(): String {
val davResource = DavResource(httpClient!!, httpUrl!!)
val responses = ArrayList<Response>()
davResource.propfind(0, CalendarHomeSet.NAME) { response, _ ->
responses.add(response)
}
val response = responses[0]
val calendarHomeSet = response[CalendarHomeSet::class.java]
?: throw DisplayableException(R.string.caldav_home_set_not_found)
val hrefs: List<String> = calendarHomeSet.hrefs
if (hrefs.size != 1) {
throw DisplayableException(R.string.caldav_home_set_not_found)
}
val homeSet = hrefs[0]
if (isNullOrEmpty(homeSet)) {
throw DisplayableException(R.string.caldav_home_set_not_found)
}
return davResource.location.resolve(homeSet).toString()
}
@Throws(IOException::class, DavException::class, NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun homeSet(): String = withContext(Dispatchers.IO) {
var principal: String? = null
try {
principal = tryFindPrincipal("/.well-known/caldav")
} catch (e: Exception) {
if (e is HttpException && e.code == 401) {
throw e
}
Timber.w(e)
}
if (principal == null) {
principal = tryFindPrincipal("")
}
forUrl(
(if (isNullOrEmpty(principal)) httpUrl else httpUrl!!.resolve(principal!!)).toString(),
basicDigestAuthHandler!!.username,
basicDigestAuthHandler.password)
.findHomeset()
}
@Throws(IOException::class, DavException::class)
suspend fun calendars(): List<Response> = withContext(Dispatchers.IO) {
val davResource = DavResource(httpClient!!, httpUrl!!)
val responses = ArrayList<Response>()
davResource.propfind(
1,
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME) { response: Response, relation: HrefRelation ->
if (relation == HrefRelation.MEMBER) {
responses.add(response)
}
}
val urls: MutableList<Response> = ArrayList()
for (member in responses) {
val resourceType = member[ResourceType::class.java]
if (resourceType == null
|| !resourceType.types.contains(CALENDAR)) {
Timber.d("%s is not a calendar", member)
continue
}
val supportedCalendarComponentSet = member.get(SupportedCalendarComponentSet::class.java)
if (supportedCalendarComponentSet == null
|| !supportedCalendarComponentSet.supportsTasks) {
Timber.d("%s does not support tasks", member)
continue
}
urls.add(member)
}
urls
}
@Throws(IOException::class, HttpException::class)
suspend fun deleteCollection() = withContext(Dispatchers.IO) {
DavResource(httpClient!!, httpUrl!!).delete(null) {}
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun makeCollection(displayName: String, color: Int): String = withContext(Dispatchers.IO) {
val davResource = DavResource(httpClient!!, httpUrl!!.resolve(UUIDHelper.newUUID() + "/")!!)
val mkcolString = getMkcolString(displayName, color)
davResource.mkCol(mkcolString) {}
davResource.location.toString()
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun updateCollection(displayName: String, color: Int): String = withContext(Dispatchers.IO) {
val davResource = PatchableDavResource(httpClient!!, httpUrl!!)
davResource.propPatch(getPropPatchString(displayName, color)) {}
davResource.location.toString()
}
@Throws(IOException::class, XmlPullParserException::class)
private fun getPropPatchString(displayName: String, color: Int): String {
val xmlPullParserFactory = XmlPullParserFactory.newInstance()
val xml = xmlPullParserFactory.newSerializer()
val stringWriter = StringWriter()
xml.setOutput(stringWriter)
xml.startDocument("UTF-8", null)
xml.setPrefix("", NS_WEBDAV)
xml.setPrefix("CAL", NS_CALDAV)
xml.setPrefix("CARD", NS_CARDDAV)
xml.startTag(NS_WEBDAV, "propertyupdate")
xml.startTag(NS_WEBDAV, "set")
xml.startTag(NS_WEBDAV, "prop")
setDisplayName(xml, displayName)
if (color != 0) {
setColor(xml, color)
}
xml.endTag(NS_WEBDAV, "prop")
xml.endTag(NS_WEBDAV, "set")
if (color == 0) {
xml.startTag(NS_WEBDAV, "remove")
xml.startTag(NS_WEBDAV, "prop")
xml.startTag(NS_APPLE_ICAL, "calendar-color")
xml.endTag(NS_APPLE_ICAL, "calendar-color")
xml.endTag(NS_WEBDAV, "prop")
xml.endTag(NS_WEBDAV, "remove")
}
xml.endTag(NS_WEBDAV, "propertyupdate")
xml.endDocument()
xml.flush()
return stringWriter.toString()
}
@Throws(IOException::class, XmlPullParserException::class)
private fun getMkcolString(displayName: String, color: Int): String {
val xmlPullParserFactory = XmlPullParserFactory.newInstance()
val xml = xmlPullParserFactory.newSerializer()
val stringWriter = StringWriter()
xml.setOutput(stringWriter)
xml.startDocument("UTF-8", null)
xml.setPrefix("", NS_WEBDAV)
xml.setPrefix("CAL", NS_CALDAV)
xml.setPrefix("CARD", NS_CARDDAV)
xml.startTag(NS_WEBDAV, "mkcol")
xml.startTag(NS_WEBDAV, "set")
xml.startTag(NS_WEBDAV, "prop")
xml.startTag(NS_WEBDAV, "resourcetype")
xml.startTag(NS_WEBDAV, "collection")
xml.endTag(NS_WEBDAV, "collection")
xml.startTag(NS_CALDAV, "calendar")
xml.endTag(NS_CALDAV, "calendar")
xml.endTag(NS_WEBDAV, "resourcetype")
setDisplayName(xml, displayName)
if (color != 0) {
setColor(xml, color)
}
xml.startTag(NS_CALDAV, "supported-calendar-component-set")
xml.startTag(NS_CALDAV, "comp")
xml.attribute(null, "name", "VTODO")
xml.endTag(NS_CALDAV, "comp")
xml.endTag(NS_CALDAV, "supported-calendar-component-set")
xml.endTag(NS_WEBDAV, "prop")
xml.endTag(NS_WEBDAV, "set")
xml.endTag(NS_WEBDAV, "mkcol")
xml.endDocument()
xml.flush()
return stringWriter.toString()
}
@Throws(IOException::class)
private fun setDisplayName(xml: XmlSerializer, name: String) {
xml.startTag(NS_WEBDAV, "displayname")
xml.text(name)
xml.endTag(NS_WEBDAV, "displayname")
}
@Throws(IOException::class)
private fun setColor(xml: XmlSerializer, color: Int) {
xml.startTag(NS_APPLE_ICAL, "calendar-color")
xml.text(String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24))
xml.endTag(NS_APPLE_ICAL, "calendar-color")
}
fun setForeground(): CaldavClient {
foreground = true
return this
}
}

@ -4,6 +4,7 @@ import android.content.Context
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.DavCalendar.Companion.MIME_ICALENDAR
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.Response.HrefRelation
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
@ -100,7 +101,7 @@ class CaldavSynchronizer @Inject constructor(
@Throws(IOException::class, DavException::class, KeyManagementException::class, NoSuchAlgorithmException::class)
private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = client.forAccount(account)
val resources = caldavClient.calendars
val resources = caldavClient.calendars()
val urls = resources.map { it.href.toString() }.toHashSet()
Timber.d("Found calendars: %s", urls)
for (calendar in caldavDao.findDeletedCalendars(account.uuid!!, ArrayList(urls))) {
@ -126,7 +127,7 @@ class CaldavSynchronizer @Inject constructor(
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
}
sync(calendar, resource, caldavClient.httpClient)
sync(calendar, resource, caldavClient.httpClient!!)
}
setError(account, "")
}
@ -141,7 +142,10 @@ class CaldavSynchronizer @Inject constructor(
}
@Throws(DavException::class)
private suspend fun sync(caldavCalendar: CaldavCalendar, resource: at.bitfire.dav4jvm.Response, httpClient: OkHttpClient) {
private suspend fun sync(
caldavCalendar: CaldavCalendar,
resource: Response,
httpClient: OkHttpClient) {
Timber.d("sync(%s)", caldavCalendar)
val httpUrl = resource.href
pushLocalChanges(caldavCalendar, httpClient, httpUrl)
@ -159,9 +163,13 @@ class CaldavSynchronizer @Inject constructor(
return
}
val davCalendar = DavCalendar(httpClient, httpUrl)
val members = ResponseList(HrefRelation.MEMBER)
davCalendar.calendarQuery("VTODO", null, null, members)
val changed = members.filter { vCard: at.bitfire.dav4jvm.Response ->
val members = ArrayList<Response>()
davCalendar.calendarQuery("VTODO", null, null) { response, relation ->
if (relation == HrefRelation.MEMBER) {
members.add(response)
}
}
val changed = members.filter { vCard: Response ->
val eTag = vCard[GetETag::class.java]
if (eTag == null || isNullOrEmpty(eTag.eTag)) {
return@filter false
@ -171,8 +179,12 @@ class CaldavSynchronizer @Inject constructor(
}
for (items in changed.chunked(30)) {
val urls = items.map { it.href }
val responses = ResponseList(HrefRelation.MEMBER)
davCalendar.multiget(urls, responses)
val responses = ArrayList<Response>()
davCalendar.multiget(urls) { response, relation ->
if (relation == HrefRelation.MEMBER) {
responses.add(response)
}
}
Timber.d("MULTI %s", urls)
for (vCard in responses) {
val eTag = vCard[GetETag::class.java]
@ -260,12 +272,12 @@ class CaldavSynchronizer @Inject constructor(
try {
val remote = DavResource(
httpClient, httpUrl.newBuilder().addPathSegment(caldavTask.`object`!!).build())
val response = Response()
remote.put(requestBody, null, false, response)
val getETag = fromResponse(response.get())
if (getETag != null && !isNullOrEmpty(getETag.eTag)) {
caldavTask.etag = getETag.eTag
caldavTask.vtodo = String(data)
remote.put(requestBody, null, false) {
val getETag = fromResponse(it)
if (getETag != null && !isNullOrEmpty(getETag.eTag)) {
caldavTask.etag = getETag.eTag
caldavTask.vtodo = String(data)
}
}
} catch (e: HttpException) {
Timber.e(e)

@ -1,19 +0,0 @@
package org.tasks.caldav;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
class Response implements Function1<okhttp3.Response, Unit> {
private okhttp3.Response response;
@Override
public Unit invoke(okhttp3.Response response) {
this.response = response;
return null;
}
public okhttp3.Response get() {
return response;
}
}

@ -1,29 +0,0 @@
package org.tasks.caldav;
import at.bitfire.dav4jvm.Response;
import at.bitfire.dav4jvm.Response.HrefRelation;
import java.util.ArrayList;
import kotlin.Unit;
import kotlin.jvm.functions.Function2;
class ResponseList extends ArrayList<Response>
implements Function2<Response, HrefRelation, Unit> {
private final HrefRelation filter;
ResponseList() {
this(null);
}
ResponseList(HrefRelation filter) {
this.filter = filter;
}
@Override
public Unit invoke(Response response, HrefRelation hrefRelation) {
if (filter == null || hrefRelation == filter) {
add(response);
}
return null;
}
}

@ -5,7 +5,7 @@ import org.tasks.ui.CompletableViewModel
class UpdateCaldavAccountViewModel @ViewModelInject constructor(
private val client: CaldavClient) : CompletableViewModel<String>() {
suspend fun updateCaldavAccount(url: String, username: String, password: String?) {
run { client.forUrl(url, username, password).homeSet }
suspend fun updateCaldavAccount(url: String, username: String, password: String) {
run { client.forUrl(url, username, password).homeSet() }
}
}

@ -11,7 +11,7 @@ class AddEteSyncAccountViewModel @ViewModelInject constructor(
run {
client.setForeground()
val token = client.forUrl(url, username, null, null).getToken(password)
Pair.create(client.forUrl(url, username, null, token!!).userInfo, token)
Pair.create(client.forUrl(url, username, null, token!!).userInfo(), token)
}
}
}

@ -16,8 +16,6 @@ import com.etesync.journalmanager.UserInfoManager
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.Completable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
@ -121,7 +119,7 @@ class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
override suspend fun addAccount(url: String, username: String, password: String) =
addAccountViewModel.addAccount(url, username, password)
override suspend fun updateAccount(url: String, username: String, password: String?) =
override suspend fun updateAccount(url: String, username: String, password: String) =
updateAccountViewModel.updateAccount(
url,
username,
@ -170,11 +168,7 @@ class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
}
override suspend fun removeAccount() {
if (caldavAccount != null) {
Completable.fromAction { eteSyncClient.forAccount(caldavAccount!!).invalidateToken() }
.subscribeOn(Schedulers.io())
.subscribe()
}
caldavAccount?.let { eteSyncClient.forAccount(it).invalidateToken() }
super.removeAccount()
}

@ -17,6 +17,8 @@ import com.etesync.journalmanager.model.SyncEntry
import com.etesync.journalmanager.util.TokenAuthenticator
import com.google.common.collect.Lists
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@ -110,7 +112,7 @@ class EteSyncClient {
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
fun forAccount(account: CaldavAccount): EteSyncClient {
suspend fun forAccount(account: CaldavAccount): EteSyncClient {
return forUrl(
account.url,
account.username,
@ -119,8 +121,8 @@ class EteSyncClient {
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteSyncClient {
return EteSyncClient(
suspend fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteSyncClient = withContext(Dispatchers.IO) {
EteSyncClient(
context,
encryption,
preferences,
@ -133,16 +135,15 @@ class EteSyncClient {
}
@Throws(IOException::class, Exceptions.HttpException::class)
fun getToken(password: String?): String? {
return JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!)
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!)
}
@get:Throws(Exceptions.HttpException::class)
val userInfo: UserInfoManager.UserInfo?
get() {
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!)
return userInfoManager.fetch(username!!)
}
@Throws(Exceptions.HttpException::class)
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) {
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!)
userInfoManager.fetch(username!!)
}
@Throws(VersionTooNewException::class, IntegrityException::class)
fun getCrypto(userInfo: UserInfoManager.UserInfo?, journal: Journal): CryptoManager {
@ -174,7 +175,7 @@ class EteSyncClient {
}
@Throws(Exceptions.HttpException::class)
fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> {
suspend fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> = withContext(Dispatchers.IO) {
val result: MutableMap<Journal, CollectionInfo> = HashMap()
for (journal in journalManager!!.list()) {
val collection = convertJournalToCollection(userInfo, journal)
@ -187,7 +188,7 @@ class EteSyncClient {
}
}
}
return result
result
}
@Throws(IntegrityException::class, Exceptions.HttpException::class, VersionTooNewException::class)
@ -195,7 +196,7 @@ class EteSyncClient {
userInfo: UserInfoManager.UserInfo?,
journal: Journal,
calendar: CaldavCalendar,
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) {
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) {
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
val crypto = getCrypto(userInfo, journal)
var journalEntries: List<JournalEntryManager.Entry>
@ -208,7 +209,7 @@ class EteSyncClient {
}
@Throws(Exceptions.HttpException::class)
fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) {
suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) {
var remoteCtag = remoteCtag
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!)
for (partition in Lists.partition(entries!!, MAX_PUSH)) {
@ -221,7 +222,7 @@ class EteSyncClient {
foreground = true
}
fun invalidateToken() {
suspend fun invalidateToken() = withContext(Dispatchers.IO) {
try {
JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!)
} catch (e: Exception) {
@ -230,7 +231,7 @@ class EteSyncClient {
}
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
fun makeCollection(name: String?, color: Int): String {
suspend fun makeCollection(name: String?, color: Int): String = withContext(Dispatchers.IO) {
val uid = Journal.genUid()
val collectionInfo = CollectionInfo()
collectionInfo.displayName = name
@ -240,29 +241,29 @@ class EteSyncClient {
collectionInfo.color = if (color == 0) null else color
val crypto = CryptoManager(collectionInfo.version, encryptionPassword!!, uid)
journalManager!!.create(Journal(crypto, collectionInfo.toJson(), uid))
return uid
uid
}
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? {
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? = withContext(Dispatchers.IO) {
val uid = calendar.url
val journal = journalManager!!.fetch(uid!!)
val userInfo = userInfo
val userInfo = userInfo()
val crypto = getCrypto(userInfo, journal)
val collectionInfo = convertJournalToCollection(userInfo, journal)
collectionInfo!!.displayName = name
collectionInfo.color = if (color == 0) null else color
journalManager.update(Journal(crypto, collectionInfo.toJson(), uid))
return uid
uid
}
@Throws(Exceptions.HttpException::class)
fun deleteCollection(calendar: CaldavCalendar) {
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) {
journalManager!!.delete(Journal.fakeWithUid(calendar.url!!))
}
@Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class)
fun createUserInfo(derivedKey: String?) {
suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) {
val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo")
val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!)
UserInfoManager(httpClient!!, httpUrl!!).create(userInfo)

@ -78,7 +78,7 @@ class EteSynchronizer @Inject constructor(
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
private suspend fun synchronize(account: CaldavAccount) {
val client = client.forAccount(account)
val userInfo = client.userInfo
val userInfo = client.userInfo()
val resources = client.getCalendars(userInfo)
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()
Timber.d("Found uids: %s", uids)

@ -12,10 +12,10 @@ class UpdateEteSyncAccountViewModel @ViewModelInject constructor(
run {
client.setForeground()
if (isNullOrEmpty(pass)) {
Pair.create(client.forUrl(url, user, null, token).userInfo, token)
Pair.create(client.forUrl(url, user, null, token).userInfo(), token)
} else {
val newToken = client.forUrl(url, user, null, null).getToken(pass)
Pair.create(client.forUrl(url, user, null, newToken).userInfo, newToken)
Pair.create(client.forUrl(url, user, null, newToken).userInfo(), newToken)
}
}
}

Loading…
Cancel
Save