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

@ -6,6 +6,6 @@ import org.tasks.ui.CompletableViewModel
class AddCaldavAccountViewModel @ViewModelInject constructor( class AddCaldavAccountViewModel @ViewModelInject constructor(
private val client: CaldavClient) : CompletableViewModel<String>() { private val client: CaldavClient) : CompletableViewModel<String>() {
suspend fun addAccount(url: String, username: String, password: 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() -> { needsValidation() -> {
showProgressIndicator() showProgressIndicator()
updateAccount(url, username, password) updateAccount(url, username, password!!)
} }
hasChanges() -> { hasChanges() -> {
updateAccount() updateAccount()
@ -238,7 +238,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
} }
protected abstract suspend fun addAccount(url: String, username: String, password: String) 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 suspend fun updateAccount()
protected abstract val helpUrl: String? protected abstract val helpUrl: String?

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

@ -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( class UpdateCaldavAccountViewModel @ViewModelInject constructor(
private val client: CaldavClient) : CompletableViewModel<String>() { private val client: CaldavClient) : CompletableViewModel<String>() {
suspend fun updateCaldavAccount(url: String, username: String, password: String?) { suspend fun updateCaldavAccount(url: String, username: String, password: String) {
run { client.forUrl(url, username, password).homeSet } run { client.forUrl(url, username, password).homeSet() }
} }
} }

@ -11,7 +11,7 @@ class AddEteSyncAccountViewModel @ViewModelInject constructor(
run { run {
client.setForeground() client.setForeground()
val token = client.forUrl(url, username, null, null).getToken(password) 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.data.Task
import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.Completable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
@ -121,7 +119,7 @@ class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
override suspend fun addAccount(url: String, username: String, password: String) = override suspend fun addAccount(url: String, username: String, password: String) =
addAccountViewModel.addAccount(url, username, password) 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( updateAccountViewModel.updateAccount(
url, url,
username, username,
@ -170,11 +168,7 @@ class EteSyncAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
} }
override suspend fun removeAccount() { override suspend fun removeAccount() {
if (caldavAccount != null) { caldavAccount?.let { eteSyncClient.forAccount(it).invalidateToken() }
Completable.fromAction { eteSyncClient.forAccount(caldavAccount!!).invalidateToken() }
.subscribeOn(Schedulers.io())
.subscribe()
}
super.removeAccount() super.removeAccount()
} }

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

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