Synchronization logging to external file

* use ExternalFileLogger to log synchronization, if enabled in Settings
* new settings: log to external file / log verbose
* DavResource: check for well-known even if service type of user-given URL can't be determined
* remove oblsete testing assets
pull/2/head
Ricki Hirner 9 years ago
parent dd50f10c58
commit 58f05986c9
No known key found for this signature in database
GPG Key ID: C4A212CF0B2B4566

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-0sec@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970714
SUMMARY:0 Sec Event
END:VEVENT
END:VCALENDAR

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-10days@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970724
SUMMARY:All-Day 10 Days
END:VEVENT
END:VCALENDAR

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:all-day-1day@example.com
DTSTAMP:20140101T000000Z
DTSTART;VALUE=DATE:19970714
DTEND;VALUE=DATE:19970714
SUMMARY:All-Day 1 Day
END:VEVENT
END:VCALENDAR

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

@ -1,11 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:event-on-that-day@example.com
DTSTAMP:19970714T170000Z
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTART;VALUE=DATE:19970714
SUMMARY:Bastille Day Party
END:VEVENT
END:VCALENDAR

@ -1,9 +0,0 @@
BEGIN:VCARD
VERSION:3.0
UID:2de59c6cc9
PRODID:-//ownCloud//NONSGML Contacts 0.2.5//EN
REV:2013-12-08T00:04:30+00:00
FN:test mctest
N:mctest;test;;;
IMPP;TYPE=WORK;X-SERVICE-TYPE=jabber:test-without-valid-scheme@test.tld
END:VCARD

@ -1,5 +0,0 @@
BEGIN:VCARD
VERSION:3.0
FN:VCard with invalid unknown properties
X-UNKNOWN@PROPERTY:MUST-NOT_CONTAIN?OTHER*LETTERS;
END:VCARD

@ -1,5 +0,0 @@
BEGIN:VCARD
VERSION:3.0
N:Äuçek;Özkan
FN:Özkan Äuçek
END:VCARD

@ -1,17 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc
SUMMARY:Recurring event with one exception
RRULE:FREQ=DAILY;COUNT=5
DTSTART;VALUE=DATE:20150501
DTEND;VALUE=DATE:20150502
END:VEVENT
BEGIN:VEVENT
UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc
RECURRENCE-ID;VALUE=DATE:20150503
DTSTART;VALUE=DATE:20150503
DTEND;VALUE=DATE:20150504
SUMMARY:Another summary for the third day
END:VEVENT
END:VCALENDAR

@ -1,16 +0,0 @@
BEGIN:VCARD
VERSION:3.0
N:Gümp;Förrest;Mr.
FN:Förrest Gümp
ORG:Bubba Gump Shrimpß Co.
TITLE:Shrimp Man
PHOTO;VALUE=URL;TYPE=PNG:http://192.168.0.11:3000/assets/davdroid-logo-192.png
TEL;TYPE=WORK,VOICE:(111) 555-1212
TEL;TYPE=HOME,VOICE:(404) 555-1212
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
REV:2008-04-24T19:52:43Z
END:VCARD

@ -1,14 +0,0 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Blabla
BEGIN:VEVENT
CLASS:PUBLIC
CREATED;VALUE=DATE-TIME:20131008T205713
LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740
SUMMARY:online Anmeldung
DESCRIPTION:http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportce
ntergroup=&day=6
UID:b99c41704b
DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20131019T060000
END:VEVENT
END:VCALENDAR

@ -1,16 +0,0 @@
BEGIN:VCARD
VERSION:3.0
N:Gump;Forrest
FN:Forrest Gump
ORG:Bubba Gump Shrimp Co.
TITLE:Shrimp Man
PHOTO;VALUE=URL;TYPE=GIF:http://www.example.com/dir_photos/my_photo.gif
TEL;TYPE=WORK,VOICE:(111) 555-1212
TEL;TYPE=HOME,VOICE:(404) 555-1212
ADR;TYPE=WORK:;;100 Waters Edge;Baytown;LA;30314;United States of America
LABEL;TYPE=WORK:100 Waters Edge\nBaytown, LA 30314\nUnited States of America
ADR;TYPE=HOME:;;42 Plantation St.;Baytown;LA;30314;United States of America
LABEL;TYPE=HOME:42 Plantation St.\nBaytown, LA 30314\nUnited States of America
EMAIL;TYPE=PREF,INTERNET:forrestgump@example.com
REV:2008-04-24T19:52:43Z
END:VCARD

@ -1,33 +0,0 @@
BEGIN:VCALENDAR
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:/freeassociation.sourceforge.net/Tzfile/Europe/Vienna
X-LIC-LOCATION:Europe/Vienna
BEGIN:STANDARD
TZNAME:CET
DTSTART:19701027T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
DTSTART:19700331T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:c252087c-7354-4722-aea9-0e7d86c01a25
DTSTAMP:20130926T151211Z
SUMMARY:Test-Ereignis im schönen Wien
DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000
DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T180000
X-RADICALE-NAME:97929342-291a-434e-bf1a-fa1749bf99d0.ics
X-EVOLUTION-CALDAV-HREF:/radicale/rfc2822/default.ics/97929342-291a-434e-bf1a-fa1749bf99d0.ics
X-EVOLUTION-CALDAV-ETAG:\"-3264224243575339985\"
END:VEVENT
END:VCALENDAR

@ -20,6 +20,7 @@
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" />
<uses-permission android:name="org.dmfs.permission.READ_TASKS" />
<uses-permission android:name="org.dmfs.permission.WRITE_TASKS" />

@ -21,6 +21,8 @@ import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import com.squareup.okhttp.logging.HttpLoggingInterceptor;
import org.slf4j.Logger;
import java.io.IOException;
import java.net.Proxy;
import java.security.KeyManagementException;
@ -39,18 +41,7 @@ import de.duenndns.ssl.MemorizingTrustManager;
import lombok.RequiredArgsConstructor;
public class HttpClient extends OkHttpClient {
protected static final String HEADER_AUTHORIZATION = "Authorization";
final static UserAgentInterceptor userAgentInterceptor = new UserAgentInterceptor();
final static HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Constants.log.trace(message);
}
});
static {
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
}
static final String userAgent;
static {
@ -58,12 +49,14 @@ public class HttpClient extends OkHttpClient {
userAgent = "DAVdroid/" + BuildConfig.VERSION_NAME + " (" + date + "; dav4android) Android/" + Build.VERSION.RELEASE;
}
final Logger log;
final Context context;
protected String username, password;
protected HttpClient(Context context) {
protected HttpClient(final Logger log, Context context) {
super();
this.log = (log != null) ? log : Constants.log;
this.context = context;
if (context != null) {
@ -90,12 +83,20 @@ public class HttpClient extends OkHttpClient {
networkInterceptors().add(userAgentInterceptor);
// enable verbose logs, if requested
if (Constants.log.isTraceEnabled())
enableLogs();
if (log.isTraceEnabled()) {
HttpLoggingInterceptor logger = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
log.trace(message);
}
});
logger.setLevel(HttpLoggingInterceptor.Level.BODY);
interceptors().add(logger);
}
}
public HttpClient(Context context, String username, String password, boolean preemptive) {
this(context);
public HttpClient(Logger log, Context context, String username, String password, boolean preemptive) {
this(log, context);
// authentication
this.username = username;
@ -113,8 +114,8 @@ public class HttpClient extends OkHttpClient {
* @param client user name and password from this client will be used
* @param host authentication will be restricted to this host
*/
public HttpClient(HttpClient client, String host) {
this(client.context);
public HttpClient(Logger log, HttpClient client, String host) {
this(log, client.context);
username = client.username;
password = client.password;
@ -123,12 +124,7 @@ public class HttpClient extends OkHttpClient {
// for testing (mock server doesn't need auth)
protected HttpClient() {
this(null, null, null, false);
}
protected void enableLogs() {
interceptors().add(loggingInterceptor);
this(null, null, null, null, false);
}
@ -142,7 +138,6 @@ public class HttpClient extends OkHttpClient {
}
}
@RequiredArgsConstructor
static class PreemptiveAuthenticationInterceptor implements Interceptor {
final String username, password;

@ -0,0 +1,348 @@
/*
* 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.log;
import org.slf4j.Logger;
import org.slf4j.Marker;
/**
* A DAVdroid logger base class that wraps all calls around some standard log() calls.
* @throws UnsupportedOperationException for all methods with Marker
* arguments (as Markers are not used by DAVdroid logging).
*/
public abstract class CustomLogger implements Logger {
private static final String
PREFIX_ERROR = "[error] ",
PREFIX_WARN = "[warn ] ",
PREFIX_INFO = "[info ] ",
PREFIX_DEBUG = "[debug] ",
PREFIX_TRACE = "[trace] ";
protected boolean verbose;
protected abstract void log(String prefix, String msg);
protected abstract void log(String prefix, String format, Object arg);
protected abstract void log(String prefix, String format, Object arg1, Object arg2);
protected abstract void log(String prefix, String format, Object... args);
protected abstract void log(String prefix, String msg, Throwable t);
// STANDARD CALLS
@Override
public boolean isTraceEnabled() {
return verbose;
}
@Override
public void trace(String msg) {
if (verbose)
log(PREFIX_TRACE, msg);
}
@Override
public void trace(String format, Object arg) {
if (verbose)
log(PREFIX_TRACE, format, arg);
}
@Override
public void trace(String format, Object arg1, Object arg2) {
if (verbose) log(PREFIX_TRACE, format, arg1, arg2);
}
@Override
public void trace(String format, Object... arguments) {
if (verbose)
log(PREFIX_TRACE, format, arguments);
}
@Override
public void trace(String msg, Throwable t) {
if (verbose)
log(PREFIX_TRACE, msg, t);
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String msg) {
log(PREFIX_DEBUG, msg);
}
@Override
public void debug(String format, Object arg) {
log(PREFIX_DEBUG, format, arg);
}
@Override
public void debug(String format, Object arg1, Object arg2) {
log(PREFIX_DEBUG, format, arg1, arg2);
}
@Override
public void debug(String format, Object... arguments) {
log(PREFIX_DEBUG, format, arguments);
}
@Override
public void debug(String msg, Throwable t) {
log(PREFIX_DEBUG, msg, t);
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public void info(String msg) {
log(PREFIX_INFO, msg);
}
@Override
public void info(String format, Object arg) {
log(PREFIX_INFO, format, arg);
}
@Override
public void info(String format, Object arg1, Object arg2) {
log(PREFIX_INFO, format, arg1, arg2);
}
@Override
public void info(String format, Object... arguments) {
log(PREFIX_INFO, format, arguments);
}
@Override
public void info(String msg, Throwable t) {
log(PREFIX_INFO, msg, t);
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public void warn(String msg) {
log(PREFIX_WARN, msg);
}
@Override
public void warn(String format, Object arg) {
log(PREFIX_WARN, format, arg);
}
@Override
public void warn(String format, Object... arguments) {
log(PREFIX_WARN, format, arguments);
}
@Override
public void warn(String format, Object arg1, Object arg2) {
log(PREFIX_WARN, format, arg1, arg2);
}
@Override
public void warn(String msg, Throwable t) {
log(PREFIX_WARN, msg, t);
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public void error(String msg) {
log(PREFIX_ERROR, msg);
}
@Override
public void error(String format, Object arg) {
log(PREFIX_ERROR, format, arg);
}
@Override
public void error(String format, Object arg1, Object arg2) {
log(PREFIX_ERROR, format, arg1, arg2);
}
@Override
public void error(String format, Object... arguments) {
log(PREFIX_ERROR, format, arguments);
}
@Override
public void error(String msg, Throwable t) {
log(PREFIX_ERROR, msg, t);
}
// CALLS WITH MARKER
@Override
public boolean isTraceEnabled(Marker marker) {
throw new UnsupportedOperationException();
}
@Override
public void trace(Marker marker, String msg) {
throw new UnsupportedOperationException();
}
@Override
public void trace(Marker marker, String format, Object arg) {
throw new UnsupportedOperationException();
}
@Override
public void trace(Marker marker, String format, Object arg1, Object arg2) {
throw new UnsupportedOperationException();
}
@Override
public void trace(Marker marker, String format, Object... argArray) {
throw new UnsupportedOperationException();
}
@Override
public void trace(Marker marker, String msg, Throwable t) {
throw new UnsupportedOperationException();
}
@Override
public boolean isDebugEnabled(Marker marker) {
throw new UnsupportedOperationException();
}
@Override
public void debug(Marker marker, String msg) {
throw new UnsupportedOperationException();
}
@Override
public void debug(Marker marker, String format, Object arg) {
throw new UnsupportedOperationException();
}
@Override
public void debug(Marker marker, String format, Object arg1, Object arg2) {
throw new UnsupportedOperationException();
}
@Override
public void debug(Marker marker, String format, Object... arguments) {
throw new UnsupportedOperationException();
}
@Override
public void debug(Marker marker, String msg, Throwable t) {
throw new UnsupportedOperationException();
}
@Override
public boolean isInfoEnabled(Marker marker) {
throw new UnsupportedOperationException();
}
@Override
public void info(Marker marker, String msg) {
throw new UnsupportedOperationException();
}
@Override
public void info(Marker marker, String format, Object arg) {
throw new UnsupportedOperationException();
}
@Override
public void info(Marker marker, String format, Object arg1, Object arg2) {
throw new UnsupportedOperationException();
}
@Override
public void info(Marker marker, String format, Object... arguments) {
throw new UnsupportedOperationException();
}
@Override
public void info(Marker marker, String msg, Throwable t) {
throw new UnsupportedOperationException();
}
@Override
public boolean isWarnEnabled(Marker marker) {
throw new UnsupportedOperationException();
}
@Override
public void warn(Marker marker, String msg) {
throw new UnsupportedOperationException();
}
@Override
public void warn(Marker marker, String format, Object arg) {
throw new UnsupportedOperationException();
}
@Override
public void warn(Marker marker, String format, Object arg1, Object arg2) {
throw new UnsupportedOperationException();
}
@Override
public void warn(Marker marker, String format, Object... arguments) {
throw new UnsupportedOperationException();
}
@Override
public void warn(Marker marker, String msg, Throwable t) {
throw new UnsupportedOperationException();
}
@Override
public boolean isErrorEnabled(Marker marker) {
throw new UnsupportedOperationException();
}
@Override
public void error(Marker marker, String msg) {
throw new UnsupportedOperationException();
}
@Override
public void error(Marker marker, String format, Object arg) {
throw new UnsupportedOperationException();
}
@Override
public void error(Marker marker, String format, Object arg1, Object arg2) {
throw new UnsupportedOperationException();
}
@Override
public void error(Marker marker, String format, Object... arguments) {
throw new UnsupportedOperationException();
}
@Override
public void error(Marker marker, String msg, Throwable t) {
throw new UnsupportedOperationException();
}
}

@ -0,0 +1,75 @@
/*
* 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.log;
import android.content.Context;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import lombok.Getter;
public class ExternalFileLogger extends CustomLogger implements Closeable {
@Getter protected final String name;
protected final PrintWriter writer;
public ExternalFileLogger(Context context, String fileName, boolean verbose) throws IOException {
this.verbose = verbose;
File dir = context.getExternalFilesDir(null);
if (dir == null)
throw new IOException("External media not available for log creation");
File log = new File(dir, name = fileName);
writer = new PrintWriter(log);
}
@Override
public void close() throws IOException {
writer.close();
}
@Override
protected void log(String prefix, String msg) {
writer.write(prefix + msg + "\n");
}
@Override
protected void log(String prefix, String format, Object arg) {
writer.write(prefix + format.replace("{}", arg.toString()) + "\n");
}
@Override
protected void log(String prefix, String format, Object arg1, Object arg2) {
writer.write(prefix + format.replaceFirst("\\{\\}", arg1.toString()).replaceFirst("\\{\\}", arg2.toString()) + "\n");
}
@Override
protected void log(String prefix, String format, Object... args) {
String message = prefix;
for (Object arg : args)
format.replaceFirst("\\{\\}", arg.toString());
writer.write(prefix + format + "\n");
}
@Override
protected void log(String prefix, String msg, Throwable t) {
writer.write(prefix + msg + " - EXCEPTION:");
t.printStackTrace(writer);
writer.write("CAUSED BY:\n");
ExceptionUtils.printRootCauseStackTrace(t, writer);
}
}

@ -12,6 +12,7 @@ import android.text.TextUtils;
import com.squareup.okhttp.HttpUrl;
import org.slf4j.Logger;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.SRVRecord;
@ -57,7 +58,8 @@ public class DavResourceFinder {
@Override public String toString() { return name; }
};
protected Context context;
protected final Logger log;
protected final Context context;
protected final HttpClient httpClient;
protected final ServerInfo serverInfo;
@ -67,11 +69,12 @@ public class DavResourceFinder {
taskLists = new HashMap<>();
public DavResourceFinder(Context context, ServerInfo serverInfo) {
public DavResourceFinder(Logger log, Context context, ServerInfo serverInfo) {
this.log = log;
this.context = context;
this.serverInfo = serverInfo;
httpClient = new HttpClient(context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);
httpClient = new HttpClient(log, context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);
}
public void findResources() throws URISyntaxException, IOException, HttpException, DavException {
@ -101,7 +104,7 @@ public class DavResourceFinder {
2. user-given URL has a calendar-home-set property (i.e. is a principal URL)
*/
Constants.log.info("Check whether user-given URL is a calendar collection and/or contains <calendar-home-set> and/or has <current-user-principal>");
DavResource davBase = new DavResource(httpClient, userURL);
DavResource davBase = new DavResource(log, httpClient, userURL);
try {
if (service == Service.CALDAV) {
davBase.propfind(0,
@ -158,18 +161,23 @@ public class DavResourceFinder {
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principalUrl = davBase.location.resolve(currentUserPrincipal.href);
if (principalUrl == null) {
Constants.log.info("User-given URL doesn't contain <current-user-principal>, trying /.well-known/" + service.name);
principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/" + service.name));
}
}
} catch(IOException|HttpException|DavException e) {
Constants.log.debug("Couldn't find <current-user-principal>", e);
Constants.log.debug("Couldn't find <current-user-principal> at user-given URL", e);
}
if (principalUrl == null)
try {
Constants.log.info("User-given URL doesn't contain <current-user-principal>, trying /.well-known/" + service.name);
principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/" + service.name));
} catch(IOException|HttpException e) {
Constants.log.debug("Couldn't determine <current-user-principal> from well-known " + service + " path", e);
}
// try service discovery with "domain" = user-given host name
domain = baseURI.getHost();
if (principalUrl == null)
// still no principal URL, try service discovery with "domain" = user-given host name
domain = baseURI.getHost();
} else if ("mailto".equals(baseURI.getScheme())) {
String mailbox = baseURI.getSchemeSpecificPart();
@ -190,7 +198,7 @@ public class DavResourceFinder {
if (principalUrl != null) {
Constants.log.info("Principal URL=" + principalUrl + ", getting <calendar-home-set>");
try {
DavResource principal = new DavResource(httpClient, principalUrl);
DavResource principal = new DavResource(log, httpClient, principalUrl);
if (service == Service.CALDAV) {
principal.propfind(0, CalendarHomeSet.NAME);
@ -226,7 +234,7 @@ public class DavResourceFinder {
if (service == Service.CALDAV)
try {
Constants.log.info("Listing calendar collections in home set " + url);
DavResource homeSet = new DavResource(httpClient, url);
DavResource homeSet = new DavResource(log, httpClient, url);
homeSet.propfind(1, SupportedCalendarComponentSet.NAME, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME,
CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME);
@ -242,7 +250,7 @@ public class DavResourceFinder {
else if (service == Service.CARDDAV)
try {
Constants.log.info("Listing address books in home set " + url);
DavResource homeSet = new DavResource(httpClient, url);
DavResource homeSet = new DavResource(log, httpClient, url);
homeSet.propfind(1, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME, AddressbookDescription.NAME);
// home set should not be an address book, but some servers have only one address book and it's the home set
@ -361,45 +369,40 @@ public class DavResourceFinder {
* @param service service to discover (CALDAV or CARDDAV)
* @return principal URL, or null if none found
*/
protected HttpUrl discoverPrincipalUrl(String domain, Service service) {
protected HttpUrl discoverPrincipalUrl(String domain, Service service) throws IOException, HttpException, DavException {
String scheme = null;
String fqdn = null;
Integer port = null;
List<String> paths = new LinkedList<>(); // there may be multiple paths to try
try {
final String query = "_" + service.name + "s._tcp." + domain;
Constants.log.debug("Looking up SRV records for " + query);
Record[] records = new Lookup(query, Type.SRV).run();
if (records != null && records.length >= 1) {
// choose SRV record to use (query may return multiple SRV records)
SRVRecord srv = selectSRVRecord(records);
scheme = "https";
fqdn = srv.getTarget().toString(true);
port = srv.getPort();
Constants.log.info("Found " + service + " service: fqdn=" + fqdn + ", port=" + port);
// look for TXT record too (for initial context path)
records = new Lookup(domain, Type.TXT).run();
if (records != null && records.length >= 1) {
TXTRecord txt = (TXTRecord)records[0];
for (String segment : (String[])txt.getStrings().toArray(new String[0]))
if (segment.startsWith("path=")) {
paths.add(segment.substring(5));
Constants.log.info("Found TXT record; initial context path=" + paths);
break;
}
}
final String query = "_" + service.name + "s._tcp." + domain;
Constants.log.debug("Looking up SRV records for " + query);
Record[] records = new Lookup(query, Type.SRV).run();
if (records != null && records.length >= 1) {
// choose SRV record to use (query may return multiple SRV records)
SRVRecord srv = selectSRVRecord(records);
scheme = "https";
fqdn = srv.getTarget().toString(true);
port = srv.getPort();
Constants.log.info("Found " + service + " service: fqdn=" + fqdn + ", port=" + port);
// if there's TXT record if it it's wrong, try well-known
paths.add("/.well-known/" + service.name);
// if this fails, too, try "/"
paths.add("/");
// look for TXT record too (for initial context path)
records = new Lookup(domain, Type.TXT).run();
if (records != null && records.length >= 1) {
TXTRecord txt = (TXTRecord)records[0];
for (String segment : (String[])txt.getStrings().toArray(new String[0]))
if (segment.startsWith("path=")) {
paths.add(segment.substring(5));
Constants.log.info("Found TXT record; initial context path=" + paths);
break;
}
}
} catch (IOException e) {
Constants.log.debug("SRV/TXT record discovery failed", e);
return null;
// if there's TXT record if it it's wrong, try well-known
paths.add("/.well-known/" + service.name);
// if this fails, too, try "/"
paths.add("/");
}
for (String path : paths) {
@ -424,20 +427,16 @@ public class DavResourceFinder {
* @param url URL to query with PROPFIND (Depth: 0)
* @return current-user-principal URL, or null if none
*/
protected HttpUrl getCurrentUserPrincipal(HttpUrl url) {
try {
DavResource dav = new DavResource(httpClient, url);
dav.propfind(0, CurrentUserPrincipal.NAME);
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal) dav.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null) {
HttpUrl principal = url.resolve(currentUserPrincipal.href);
if (principal != null) {
Constants.log.info("Found current-user-principal: " + principal);
return principal;
}
protected HttpUrl getCurrentUserPrincipal(HttpUrl url) throws IOException, HttpException, DavException {
DavResource dav = new DavResource(log, httpClient, url);
dav.propfind(0, CurrentUserPrincipal.NAME);
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal) dav.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null) {
HttpUrl principal = url.resolve(currentUserPrincipal.href);
if (principal != null) {
Constants.log.info("Found current-user-principal: " + principal);
return principal;
}
} catch(IOException|HttpException|DavException e) {
Constants.log.debug("PROPFIND for current-user-principal on " + url + " failed", e);
}
return null;
}

@ -47,6 +47,8 @@ public class AccountSettings {
KEY_USERNAME = "user_name",
KEY_AUTH_PREEMPTIVE = "auth_preemptive",
KEY_LOG_TO_EXTERNAL_FILE = "log_external_file",
KEY_LOG_VERBOSE = "log_verbose",
KEY_LAST_ANDROID_VERSION = "last_android_version";
public final static long SYNC_INTERVAL_MANUALLY = -1;
@ -84,9 +86,8 @@ public class AccountSettings {
showNotification(Constants.NOTIFICATION_ANDROID_VERSION_UPDATED,
context.getString(R.string.settings_android_update_title),
context.getString(R.string.settings_android_update_description));
accountManager.setUserData(account, KEY_LAST_ANDROID_VERSION, String.valueOf(Build.VERSION.SDK_INT));
}
accountManager.setUserData(account, KEY_LAST_ANDROID_VERSION, String.valueOf(Build.VERSION.SDK_INT));
}
}
@ -119,18 +120,23 @@ public class AccountSettings {
// authentication settings
public String getUserName() {
return accountManager.getUserData(account, KEY_USERNAME);
}
public void setUserName(String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
public String username() { return accountManager.getUserData(account, KEY_USERNAME); }
public void username(String userName) { accountManager.setUserData(account, KEY_USERNAME, userName); }
public String getPassword() {
return accountManager.getPassword(account);
}
public void setPassword(String password) { accountManager.setPassword(account, password); }
public String password() { return accountManager.getPassword(account); }
public void password(String password) { accountManager.setPassword(account, password); }
public boolean getPreemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
public void setPreemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
public boolean preemptiveAuth() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); }
public void preemptiveAuth(boolean preemptive) { accountManager.setUserData(account, KEY_AUTH_PREEMPTIVE, Boolean.toString(preemptive)); }
// logging settings
public boolean logToExternalFile() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_LOG_TO_EXTERNAL_FILE)); }
public void logToExternalFile(boolean newValue) { accountManager.setUserData(account, KEY_LOG_TO_EXTERNAL_FILE, Boolean.toString(newValue)); }
public boolean logVerbose() { return Boolean.parseBoolean(accountManager.getUserData(account, KEY_LOG_VERBOSE)); }
public void logVerbose(boolean newValue) { accountManager.setUserData(account, KEY_LOG_VERBOSE, Boolean.toString(newValue)); }
// sync. settings
@ -229,7 +235,7 @@ public class AccountSettings {
}
private void update_1_2() throws ContactsStorageException {
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),,
/* - KEY_ADDRESSBOOK_URL ("addressbook_url"),
- KEY_ADDRESSBOOK_CTAG ("addressbook_ctag"),
- KEY_ADDRESSBOOK_VCARD_VERSION ("addressbook_vcard_version") are not used anymore (now stored in ContactsContract.SyncState)
- KEY_LAST_ANDROID_VERSION ("last_android_version") has been added
@ -237,22 +243,25 @@ public class AccountSettings {
// move previous address book info to ContactsContract.SyncState
@Cleanup("release") ContentProviderClient provider = context.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
if (provider != null) {
LocalAddressBook addr = new LocalAddressBook(account, provider);
String url = accountManager.getUserData(account, "addressbook_url");
if (!TextUtils.isEmpty(url))
addr.setURL(url);
accountManager.setUserData(account, "addressbook_url", null);
String cTag = accountManager.getUserData(account, "addressbook_ctag");
if (!TextUtils.isEmpty(cTag))
addr.setCTag(cTag);
accountManager.setUserData(account, "addressbook_ctag", null);
}
// store current Android version
accountManager.setUserData(account, KEY_LAST_ANDROID_VERSION, String.valueOf(Build.VERSION.SDK_INT));
if (provider == null)
throw new ContactsStorageException("Couldn't access Contacts provider");
LocalAddressBook addr = new LocalAddressBook(account, provider);
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
ContentValues values = new ContentValues();
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
addr.updateSettings(values);
String url = accountManager.getUserData(account, "addressbook_url");
if (!TextUtils.isEmpty(url))
addr.setURL(url);
accountManager.setUserData(account, "addressbook_url", null);
String cTag = accountManager.getUserData(account, "addressbook_ctag");
if (!TextUtils.isEmpty(cTag))
addr.setCTag(cTag);
accountManager.setUserData(account, "addressbook_ctag", null);
accountManager.setUserData(account, KEY_SETTINGS_VERSION, "2");
}

@ -74,7 +74,7 @@ public class CalendarSyncManager extends SyncManager {
Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j
collectionURL = HttpUrl.parse(localCalendar().getName());
davCollection = new DavCalendar(httpClient, collectionURL);
davCollection = new DavCalendar(log, httpClient, collectionURL);
}
@Override
@ -90,7 +90,7 @@ public class CalendarSyncManager extends SyncManager {
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
ContentValues values = new ContentValues(2);
Constants.log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
values.put(Calendars.CALENDAR_COLOR, color);
localCalendar().update(values);
@ -119,20 +119,20 @@ public class CalendarSyncManager extends SyncManager {
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VEVENT: " + fileName);
log.debug("Found remote VEVENT: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
Constants.log.info("Downloading " + toDownload.size() + " events (" + MAX_MULTIGET + " at once)");
log.info("Downloading " + toDownload.size() + " events (" + 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;
Constants.log.info("Downloading " + StringUtils.join(bunch, ", "));
log.info("Downloading " + StringUtils.join(bunch, ", "));
if (bunch.length == 1) {
// only one contact, use GET
@ -190,7 +190,7 @@ public class CalendarSyncManager extends SyncManager {
try {
events = Event.fromStream(stream, charset);
} catch (InvalidCalendarException e) {
Constants.log.error("Received invalid iCalendar, ignoring");
log.error("Received invalid iCalendar, ignoring");
return;
}
@ -200,18 +200,18 @@ public class CalendarSyncManager extends SyncManager {
// delete local event, if it exists
LocalEvent localEvent = (LocalEvent)localResources.get(fileName);
if (localEvent != null) {
Constants.log.info("Updating " + fileName + " in local calendar");
log.info("Updating " + fileName + " in local calendar");
localEvent.setETag(eTag);
localEvent.update(newData);
syncResult.stats.numUpdates++;
} else {
Constants.log.info("Adding " + fileName + " to local calendar");
log.info("Adding " + fileName + " to local calendar");
localEvent = new LocalEvent(localCalendar(), newData, fileName, eTag);
localEvent.add();
syncResult.stats.numInserts++;
}
} else
Constants.log.error("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
log.error("Received VCALENDAR with not exactly one VEVENT with UID, but without RECURRENCE-ID; ignoring " + fileName);
}
}

@ -83,7 +83,7 @@ public class ContactsSyncManager extends SyncManager {
if (url == null)
throw new ContactsStorageException("Couldn't get address book URL");
collectionURL = HttpUrl.parse(url);
davCollection = new DavAddressBook(httpClient, collectionURL);
davCollection = new DavAddressBook(log, httpClient, collectionURL);
processChangedGroups();
}
@ -98,7 +98,7 @@ public class ContactsSyncManager extends SyncManager {
for (MediaType type : supportedAddressData.types)
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
hasVCard4 = true;
Constants.log.info("Server advertises VCard/4 support: " + hasVCard4);
log.info("Server advertises VCard/4 support: " + hasVCard4);
}
@Override
@ -118,7 +118,7 @@ public class ContactsSyncManager extends SyncManager {
davAddressBook().addressbookQuery();
} catch(HttpException e) {
if (e.status/100 == 4) {
Constants.log.warn("Server error on REPORT addressbook query, falling back to PROPFIND", e);
log.warn("Server error on REPORT addressbook query, falling back to PROPFIND", e);
davAddressBook().propfind(1, GetETag.NAME);
}
}
@ -126,14 +126,14 @@ public class ContactsSyncManager extends SyncManager {
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VCard: " + fileName);
log.debug("Found remote VCard: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException {
Constants.log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
// prepare downloader which may be used to download external resource like contact photos
Contact.Downloader downloader = new ResourceDownloader(httpClient, collectionURL);
@ -143,7 +143,7 @@ public class ContactsSyncManager extends SyncManager {
if (Thread.interrupted())
return;
Constants.log.info("Downloading " + StringUtils.join(bunch, ", "));
log.info("Downloading " + StringUtils.join(bunch, ", "));
if (bunch.length == 1) {
// only one contact, use GET
@ -202,7 +202,7 @@ public class ContactsSyncManager extends SyncManager {
// groups with DELETED=1: remove group finally
for (LocalGroup group : addressBook.getDeletedGroups()) {
long groupId = group.getId();
Constants.log.debug("Finally removing group #" + groupId);
log.debug("Finally removing group #" + groupId);
// remove group memberships, but not as sync adapter (should marks contacts as DIRTY)
// NOTE: doesn't work that way because Contact Provider removes the group memberships even for DELETED groups
// addressBook.removeGroupMemberships(groupId, false);
@ -212,7 +212,7 @@ public class ContactsSyncManager extends SyncManager {
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (LocalGroup group : addressBook.getDirtyGroups()) {
long groupId = group.getId();
Constants.log.debug("Marking members of modified group #" + groupId + " as dirty");
log.debug("Marking members of modified group #" + groupId + " as dirty");
addressBook.markMembersDirty(groupId);
group.clearDirty();
}
@ -226,32 +226,32 @@ public class ContactsSyncManager extends SyncManager {
// update local contact, if it exists
LocalContact localContact = (LocalContact)localResources.get(fileName);
if (localContact != null) {
Constants.log.info("Updating " + fileName + " in local address book");
log.info("Updating " + fileName + " in local address book");
localContact.eTag = eTag;
localContact.update(newData);
syncResult.stats.numUpdates++;
} else {
Constants.log.info("Adding " + fileName + " to local address book");
log.info("Adding " + fileName + " to local address book");
localContact = new LocalContact(localAddressBook(), newData, fileName, eTag);
localContact.add();
syncResult.stats.numInserts++;
}
} else
Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName);
log.error("Received VCard with not exactly one VCARD, ignoring " + fileName);
}
// downloader helper class
@RequiredArgsConstructor
private static class ResourceDownloader implements Contact.Downloader {
private class ResourceDownloader implements Contact.Downloader {
final HttpClient httpClient;
final HttpUrl baseUrl;
@Override
public byte[] download(String url, String accepts) {
HttpUrl httpUrl = HttpUrl.parse(url);
HttpClient resourceClient = new HttpClient(httpClient, httpUrl.host());
HttpClient resourceClient = new HttpClient(log, httpClient, httpUrl.host());
try {
Response response = resourceClient.newCall(new Request.Builder()
.get()
@ -264,10 +264,10 @@ public class ContactsSyncManager extends SyncManager {
if (response.isSuccessful() && stream != null) {
return IOUtils.toByteArray(stream);
} else
Constants.log.error("Couldn't download external resource");
log.error("Couldn't download external resource");
}
} catch(IOException e) {
Constants.log.error("Couldn't download external resource", e);
log.error("Couldn't download external resource", e);
}
return null;
}

@ -1,4 +1,4 @@
/*
/*
* 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
@ -22,6 +22,8 @@ import android.text.TextUtils;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.RequestBody;
import org.slf4j.Logger;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
@ -42,6 +44,7 @@ import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.log.ExternalFileLogger;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.ui.DebugInfoActivity;
@ -75,6 +78,8 @@ abstract public class SyncManager {
protected final AccountSettings settings;
protected LocalCollection localCollection;
protected Logger log;
protected final HttpClient httpClient;
protected HttpUrl collectionURL;
protected DavResource davCollection;
@ -101,9 +106,19 @@ abstract public class SyncManager {
this.authority = authority;
this.syncResult = syncResult;
// get account settings and generate httpClient
// get account settings and log to file (if requested)
settings = new AccountSettings(context, account);
httpClient = new HttpClient(context, settings.getUserName(), settings.getPassword(), settings.getPreemptiveAuth());
try {
if (settings.logToExternalFile())
log = new ExternalFileLogger(context, "davdroid-SyncManager-" + account.name + "-" + authority + ".txt", settings.logVerbose());
} catch(IOException e) {
log.error("Couldn't log to external file", e);
}
if (log == null)
log = Constants.log;
// create HttpClient with given logger
httpClient = new HttpClient(log, context, settings.username(), settings.password(), settings.preemptiveAuth());
// dismiss previous error notifications
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@ -115,60 +130,60 @@ abstract public class SyncManager {
public void performSync() {
int syncPhase = SYNC_PHASE_PREPARE;
try {
Constants.log.info("Preparing synchronization");
log.info("Preparing synchronization");
prepare();
if (Thread.interrupted())
return;
syncPhase = SYNC_PHASE_QUERY_CAPABILITIES;
Constants.log.info("Querying capabilities");
log.info("Querying capabilities");
queryCapabilities();
syncPhase = SYNC_PHASE_PROCESS_LOCALLY_DELETED;
Constants.log.info("Processing locally deleted entries");
log.info("Processing locally deleted entries");
processLocallyDeleted();
if (Thread.interrupted())
return;
syncPhase = SYNC_PHASE_PREPARE_DIRTY;
Constants.log.info("Locally preparing dirty entries");
log.info("Locally preparing dirty entries");
prepareDirty();
syncPhase = SYNC_PHASE_UPLOAD_DIRTY;
Constants.log.info("Uploading dirty entries");
log.info("Uploading dirty entries");
uploadDirty();
syncPhase = SYNC_PHASE_CHECK_SYNC_STATE;
Constants.log.info("Checking sync state");
log.info("Checking sync state");
if (checkSyncState()) {
syncPhase = SYNC_PHASE_LIST_LOCAL;
Constants.log.info("Listing local entries");
log.info("Listing local entries");
listLocal();
if (Thread.interrupted())
return;
syncPhase = SYNC_PHASE_LIST_REMOTE;
Constants.log.info("Listing remote entries");
log.info("Listing remote entries");
listRemote();
if (Thread.interrupted())
return;
syncPhase = SYNC_PHASE_COMPARE_LOCAL_REMOTE;
Constants.log.info("Comparing local/remote entries");
log.info("Comparing local/remote entries");
compareLocalRemote();
syncPhase = SYNC_PHASE_DOWNLOAD_REMOTE;
Constants.log.info("Downloading remote entries");
log.info("Downloading remote entries");
downloadRemote();
syncPhase = SYNC_PHASE_SAVE_SYNC_STATE;
Constants.log.info("Saving sync state");
log.info("Saving sync state");
saveSyncState();
} else
Constants.log.info("Remote collection didn't change, skipping remote sync");
log.info("Remote collection didn't change, skipping remote sync");
} catch (IOException|ServiceUnavailableException e) {
Constants.log.error("I/O exception during sync, trying again later", e);
log.error("I/O exception during sync, trying again later", e);
syncResult.stats.numIoExceptions++;
if (e instanceof ServiceUnavailableException) {
@ -183,19 +198,19 @@ abstract public class SyncManager {
final int messageString;
if (e instanceof UnauthorizedException) {
Constants.log.error("Not authorized anymore", e);
log.error("Not authorized anymore", e);
messageString = R.string.sync_error_unauthorized;
syncResult.stats.numAuthExceptions++;
} else if (e instanceof HttpException || e instanceof DavException) {
Constants.log.error("HTTP/DAV Exception during sync", e);
log.error("HTTP/DAV Exception during sync", e);
messageString = R.string.sync_error_http_dav;
syncResult.stats.numParseExceptions++;
} else if (e instanceof CalendarStorageException || e instanceof ContactsStorageException) {
Constants.log.error("Couldn't access local storage", e);
log.error("Couldn't access local storage", e);
messageString = R.string.sync_error_local_storage;
syncResult.databaseError = true;
} else {
Constants.log.error("Unknown sync error", e);
log.error("Unknown sync error", e);
messageString = R.string.sync_error;
syncResult.stats.numParseExceptions++;
}
@ -237,6 +252,13 @@ abstract public class SyncManager {
notification = builder.getNotification();
}
notificationManager.notify(account.name, notificationId, notification);
} finally {
if (log instanceof ExternalFileLogger)
try {
((ExternalFileLogger)log).close();
} catch (IOException e) {
Constants.log.error("Couldn't close external log file", e);
}
}
}
@ -259,15 +281,15 @@ abstract public class SyncManager {
final String fileName = local.getFileName();
if (!TextUtils.isEmpty(fileName)) {
Constants.log.info(fileName + " has been deleted locally -> deleting from server");
log.info(fileName + " has been deleted locally -> deleting from server");
try {
new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
new DavResource(log, httpClient, collectionURL.newBuilder().addPathSegment(fileName).build())
.delete(local.getETag());
} catch (IOException|HttpException e) {
Constants.log.warn("Couldn't delete " + fileName + " from server; ignoring (may be downloaded again)");
log.warn("Couldn't delete " + fileName + " from server; ignoring (may be downloaded again)");
}
} else
Constants.log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
log.info("Removing local record #" + local.getId() + " which has been deleted locally and was never uploaded");
local.delete();
syncResult.stats.numDeletes++;
}
@ -277,7 +299,7 @@ abstract public class SyncManager {
// assign file names and UIDs to new contacts so that we can use the file name as an index
for (LocalResource local : localCollection.getWithoutFileName()) {
String uuid = UUID.randomUUID().toString();
Constants.log.info("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
log.info("Found local record #" + local.getId() + " without file name; assigning file name/UID based on " + uuid);
local.updateFileNameAndUID(uuid);
}
}
@ -296,7 +318,7 @@ abstract public class SyncManager {
final String fileName = local.getFileName();
DavResource remote = new DavResource(httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
DavResource remote = new DavResource(log, httpClient, collectionURL.newBuilder().addPathSegment(fileName).build());
// generate entity to upload (VCard, iCal, whatever)
RequestBody body = prepareUpload(local);
@ -304,25 +326,25 @@ abstract public class SyncManager {
try {
if (local.getETag() == null) {
Constants.log.info("Uploading new record " + fileName);
log.info("Uploading new record " + fileName);
remote.put(body, null, true);
} else {
Constants.log.info("Uploading locally modified record " + fileName);
log.info("Uploading locally modified record " + fileName);
remote.put(body, local.getETag(), false);
}
} catch (ConflictException|PreconditionFailedException e) {
// we can't interact with the user to resolve the conflict, so we treat 409 like 412
Constants.log.info("Resource has been modified on the server before upload, ignoring", e);
log.info("Resource has been modified on the server before upload, ignoring", e);
}
String eTag = null;
GetETag newETag = (GetETag) remote.properties.get(GetETag.NAME);
if (newETag != null) {
eTag = newETag.eTag;
Constants.log.debug("Received new ETag=" + eTag + " after uploading");
log.debug("Received new ETag=" + eTag + " after uploading");
} else
Constants.log.debug("Didn't receive new ETag after uploading, setting to null");
log.debug("Didn't receive new ETag after uploading, setting to null");
local.clearDirty(eTag);
}
@ -343,12 +365,12 @@ abstract public class SyncManager {
String localCTag = null;
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL))
Constants.log.info("Manual sync, ignoring CTag");
log.info("Manual sync, ignoring CTag");
else
localCTag = localCollection.getCTag();
if (remoteCTag != null && remoteCTag.equals(localCTag)) {
Constants.log.info("Remote collection didn't change (CTag=" + remoteCTag + "), no need to query children");
log.info("Remote collection didn't change (CTag=" + remoteCTag + "), no need to query children");
return false;
} else
return true;
@ -362,7 +384,7 @@ abstract public class SyncManager {
LocalResource[] localList = localCollection.getAll();
localResources = new HashMap<>(localList.length);
for (LocalResource resource : localList) {
Constants.log.debug("Found local resource: " + resource.getFileName());
log.debug("Found local resource: " + resource.getFileName());
localResources.put(resource.getFileName(), resource);
}
}
@ -389,7 +411,7 @@ abstract public class SyncManager {
for (String localName : localResources.keySet()) {
DavResource remote = remoteResources.get(localName);
if (remote == null) {
Constants.log.info(localName + " is not on server anymore, deleting");
log.info(localName + " is not on server anymore, deleting");
localResources.get(localName).delete();
syncResult.stats.numDeletes++;
} else {
@ -402,7 +424,7 @@ abstract public class SyncManager {
if (remoteETag.equals(localETag))
syncResult.stats.numSkippedEntries++;
else {
Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")");
toDownload.add(remote);
}
@ -413,7 +435,7 @@ abstract public class SyncManager {
// add all unseen (= remotely added) remote contacts
if (!remoteResources.isEmpty()) {
Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteResources.keySet()));
log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteResources.keySet()));
toDownload.addAll(remoteResources.values());
}
}
@ -428,7 +450,7 @@ abstract public class SyncManager {
/* Save sync state (CTag). It doesn't matter if it has changed during the sync process
(for instance, because another client has uploaded changes), because this will simply
cause all remote entries to be listed at the next sync. */
Constants.log.info("Saving CTag=" + remoteCTag);
log.info("Saving CTag=" + remoteCTag);
localCollection.setCTag(remoteCTag);
}

@ -79,7 +79,7 @@ public class TasksSyncManager extends SyncManager {
Thread.currentThread().setContextClassLoader(context.getClassLoader()); // required for ical4j
collectionURL = HttpUrl.parse(localTaskList().getSyncId());
davCollection = new DavCalendar(httpClient, collectionURL);
davCollection = new DavCalendar(log, httpClient, collectionURL);
}
@Override
@ -95,7 +95,7 @@ public class TasksSyncManager extends SyncManager {
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
ContentValues values = new ContentValues(2);
Constants.log.info("Setting new task list name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
log.info("Setting new task list name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
values.put(TaskLists.LIST_NAME, displayName);
values.put(TaskLists.LIST_COLOR, color);
localTaskList().update(values);
@ -117,21 +117,21 @@ public class TasksSyncManager extends SyncManager {
remoteResources = new HashMap<>(davCollection.members.size());
for (DavResource vCard : davCollection.members) {
String fileName = vCard.fileName();
Constants.log.debug("Found remote VTODO: " + fileName);
log.debug("Found remote VTODO: " + fileName);
remoteResources.put(fileName, vCard);
}
}
@Override
protected void downloadRemote() throws IOException, HttpException, DavException, CalendarStorageException {
Constants.log.info("Downloading " + toDownload.size() + " tasks (" + MAX_MULTIGET + " at once)");
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;
Constants.log.info("Downloading " + StringUtils.join(bunch, ", "));
log.info("Downloading " + StringUtils.join(bunch, ", "));
if (bunch.length == 1) {
// only one contact, use GET
@ -189,7 +189,7 @@ public class TasksSyncManager extends SyncManager {
try {
tasks = Task.fromStream(stream, charset);
} catch (InvalidCalendarException e) {
Constants.log.error("Received invalid iCalendar, ignoring", e);
log.error("Received invalid iCalendar, ignoring", e);
return;
}
@ -199,18 +199,18 @@ public class TasksSyncManager extends SyncManager {
// update local task, if it exists
LocalTask localTask = (LocalTask)localResources.get(fileName);
if (localTask != null) {
Constants.log.info("Updating " + fileName + " in local tasklist");
log.info("Updating " + fileName + " in local tasklist");
localTask.setETag(eTag);
localTask.update(newData);
syncResult.stats.numUpdates++;
} else {
Constants.log.info("Adding " + fileName + " to local task list");
log.info("Adding " + fileName + " to local task list");
localTask = new LocalTask(localTaskList(), newData, fileName, eTag);
localTask.add();
syncResult.stats.numInserts++;
}
} else
Constants.log.error("Received VCALENDAR with not exactly one VTODO; ignoring " + fileName);
log.error("Received VCALENDAR with not exactly one VTODO; ignoring " + fileName);
}
}

@ -26,7 +26,6 @@ public class AccountActivity extends Activity {
getActionBar().setDisplayHomeAsUpEnabled(true);
final FragmentManager fm = getFragmentManager();
AccountFragment fragment = (AccountFragment)fm.findFragmentById(R.id.account_fragment);
if (fragment == null) {
fragment = new AccountFragment();
@ -36,8 +35,9 @@ public class AccountActivity extends Activity {
fragment.setArguments(args);
getFragmentManager().beginTransaction()
.add(R.id.account_fragment, fragment)
.add(R.id.account_fragment, fragment, SettingsActivity.TAG_ACCOUNT_SETTINGS)
.commit();
}
}
}

@ -9,8 +9,10 @@
package at.bitfire.davdroid.ui.settings;
import android.accounts.Account;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
@ -22,7 +24,6 @@ import android.provider.ContactsContract;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.syncadapter.AccountSettings;
import at.bitfire.ical4android.TaskProvider;
import ezvcard.VCardVersion;
public class AccountFragment extends PreferenceFragment {
final static String ARG_ACCOUNT = "account";
@ -36,44 +37,41 @@ public class AccountFragment extends PreferenceFragment {
addPreferencesFromResource(R.xml.settings_account_prefs);
account = getArguments().getParcelable(ARG_ACCOUNT);
readFromAccount();
refresh();
}
public void readFromAccount() {
public void refresh() {
final AccountSettings settings = new AccountSettings(getActivity(), account);
// category: authentication
final EditTextPreference prefUserName = (EditTextPreference)findPreference("username");
prefUserName.setSummary(settings.getUserName());
prefUserName.setText(settings.getUserName());
prefUserName.setSummary(settings.username());
prefUserName.setText(settings.username());
prefUserName.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setUserName((String)newValue);
readFromAccount();
return true;
settings.username((String) newValue);
refresh(); return false;
}
});
final EditTextPreference prefPassword = (EditTextPreference)findPreference("password");
prefPassword.setText(settings.getPassword());
prefPassword.setText(settings.password());
prefPassword.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setPassword((String)newValue);
readFromAccount();
return true;
settings.password((String) newValue);
refresh(); return false;
}
});
final SwitchPreference prefPreemptive = (SwitchPreference)findPreference("preemptive");
prefPreemptive.setChecked(settings.getPreemptiveAuth());
prefPreemptive.setChecked(settings.preemptiveAuth());
prefPreemptive.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setPreemptiveAuth((Boolean)newValue);
readFromAccount();
return true;
settings.preemptiveAuth((Boolean) newValue);
refresh(); return false;
}
});
@ -90,8 +88,7 @@ public class AccountFragment extends PreferenceFragment {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(ContactsContract.AUTHORITY, Long.parseLong((String) newValue));
readFromAccount();
return true;
refresh(); return false;
}
});
} else {
@ -111,8 +108,7 @@ public class AccountFragment extends PreferenceFragment {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(CalendarContract.AUTHORITY, Long.parseLong((String) newValue));
readFromAccount();
return true;
refresh(); return false;
}
});
} else {
@ -132,8 +128,7 @@ public class AccountFragment extends PreferenceFragment {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Long.parseLong((String) newValue));
readFromAccount();
return true;
refresh(); return false;
}
});
} else {
@ -141,5 +136,80 @@ public class AccountFragment extends PreferenceFragment {
prefSyncTasks.setSummary(R.string.settings_sync_summary_not_available);
}
// category: debug info
final SwitchPreference prefLogExternalFile = (SwitchPreference)findPreference("log_external_file");
prefLogExternalFile.setChecked(settings.logToExternalFile());
prefLogExternalFile.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
Boolean external = (Boolean)newValue;
if (external) {
getFragmentManager().beginTransaction()
.add(LogExternalFileDialogFragment.newInstance(account), null)
.commit();
return false;
} else {
settings.logToExternalFile(false);
refresh(); return false;
}
}
});
final SwitchPreference prefLogVerbose = (SwitchPreference)findPreference("log_verbose");
prefLogVerbose.setChecked(settings.logVerbose());
prefLogVerbose.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.logVerbose((Boolean) newValue);
refresh(); return false;
}
});
}
public static class LogExternalFileDialogFragment extends DialogFragment {
private static final String
KEY_ACCOUNT = "account";
public static LogExternalFileDialogFragment newInstance(Account account) {
Bundle args = new Bundle();
args.putParcelable(KEY_ACCOUNT, account);
LogExternalFileDialogFragment fragment = new LogExternalFileDialogFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public AlertDialog onCreateDialog(Bundle savedInstanceState) {
final AccountSettings settings = new AccountSettings(getActivity(), (Account)getArguments().getParcelable(KEY_ACCOUNT));
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.settings_security_warning)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.settings_log_to_external_file_confirmation)
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
settings.logToExternalFile(false);
refresh();
}
})
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
settings.logToExternalFile(true);
refresh();
}
})
.create();
}
private void refresh() {
AccountFragment fragment = (AccountFragment)getActivity().getFragmentManager().findFragmentByTag(SettingsActivity.TAG_ACCOUNT_SETTINGS);
if (fragment != null)
fragment.refresh();
}
}
}

@ -17,7 +17,7 @@ import android.os.Bundle;
import at.bitfire.davdroid.R;
public class SettingsActivity extends Activity {
private final static String KEY_SELECTED_ACCOUNT = "selected_account";
public final static String TAG_ACCOUNT_SETTINGS = "account_settings";
boolean tabletLayout;
@ -44,7 +44,7 @@ public class SettingsActivity extends Activity {
getFragmentManager().beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.replace(R.id.right_pane, fragment)
.replace(R.id.right_pane, fragment, TAG_ACCOUNT_SETTINGS)
.commit();
} else { // phone layout
Intent intent = new Intent(getApplicationContext(), AccountActivity.class);

@ -110,7 +110,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
);
try {
DavResourceFinder finder = new DavResourceFinder(context, serverInfo);
DavResourceFinder finder = new DavResourceFinder(null, context, serverInfo);
finder.findResources();
} catch (URISyntaxException e) {
serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage()));

@ -154,10 +154,15 @@
<item>Every 4 hours</item>
<item>Once a day</item>
</string-array>
<string name="settings_carddav">Address book</string>
<string name="settings_carddav_vcard4_support">VCard 4.0 support</string>
<string name="settings_carddav_vcard4_supported">Contacts are sent in VCard 4.0 format</string>
<string name="settings_carddav_vcard4_not_supported">Contacts are sent in VCard 3.0 format</string>
<string name="settings_debug">Debugging</string>
<string name="settings_security_warning">Potential security risk!</string>
<string name="settings_log_to_external_file">Log to external file</string>
<string name="settings_log_to_external_file_confirmation">External log files will contain private data and be accessible by other apps. Turn off external logging and delete the log files after use.</string>
<string name="settings_log_to_external_file_on">Logs are written to external files (if possible)</string>
<string name="settings_log_to_external_file_off">Logs are written to ADB</string>
<string name="settings_log_verbose">Verbose logging</string>
<string name="settings_log_verbose_on">Log synchronization information and network traffic</string>
<string name="settings_log_verbose_off">Log only synchronization information</string>
<string name="settings_android_update_title">Android version update</string>
<string name="settings_android_update_description">Android version updates may have an impact on how DAVdroid works. If there are problems, please delete your DAVdroid accounts and add them again.</string>

@ -59,10 +59,22 @@
</PreferenceCategory>
<!--
<PreferenceCategory android:title="@string/settings_carddav">
<PreferenceCategory android:title="@string/settings_debug">
<SwitchPreference
android:key="log_external_file"
android:persistent="false"
android:title="@string/settings_log_to_external_file"
android:summaryOn="@string/settings_log_to_external_file_on"
android:summaryOff="@string/settings_log_to_external_file_off" />
<SwitchPreference
android:key="log_verbose"
android:persistent="false"
android:title="@string/settings_log_verbose"
android:summaryOn="@string/settings_log_verbose_on"
android:summaryOff="@string/settings_log_verbose_off" />
</PreferenceCategory>
-->
</PreferenceScreen>

@ -1 +1 @@
Subproject commit e7218aeb8ad8a96620208140bc7b86f63bf05fad
Subproject commit 47541c169b970a6991b0f2f6c589232009e258ad
Loading…
Cancel
Save