mirror of
https://github.com/etesync/android
synced 2024-12-27 08:58:09 +00:00
New resource detection
* new resource detection: only CalDAV yet
This commit is contained in:
parent
e34abf291e
commit
18542adb2c
@ -23,14 +23,16 @@ import org.xbill.DNS.Type;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import at.bitfire.dav4android.DavResource;
|
import at.bitfire.dav4android.DavResource;
|
||||||
|
import at.bitfire.dav4android.UrlUtils;
|
||||||
import at.bitfire.dav4android.exception.DavException;
|
import at.bitfire.dav4android.exception.DavException;
|
||||||
import at.bitfire.dav4android.exception.HttpException;
|
import at.bitfire.dav4android.exception.HttpException;
|
||||||
import at.bitfire.dav4android.property.AddressbookDescription;
|
import at.bitfire.dav4android.exception.NotFoundException;
|
||||||
import at.bitfire.dav4android.property.AddressbookHomeSet;
|
|
||||||
import at.bitfire.dav4android.property.CalendarColor;
|
import at.bitfire.dav4android.property.CalendarColor;
|
||||||
import at.bitfire.dav4android.property.CalendarDescription;
|
import at.bitfire.dav4android.property.CalendarDescription;
|
||||||
import at.bitfire.dav4android.property.CalendarHomeSet;
|
import at.bitfire.dav4android.property.CalendarHomeSet;
|
||||||
@ -41,147 +43,289 @@ import at.bitfire.dav4android.property.DisplayName;
|
|||||||
import at.bitfire.dav4android.property.ResourceType;
|
import at.bitfire.dav4android.property.ResourceType;
|
||||||
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
|
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.DavUtils;
|
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
public class DavResourceFinder {
|
public class DavResourceFinder {
|
||||||
private final static String TAG = "davdroid.ResourceFinder";
|
private final static String TAG = "davdroid.ResourceFinder";
|
||||||
|
|
||||||
final protected Context context;
|
protected Context context;
|
||||||
|
protected final HttpClient httpClient;
|
||||||
|
protected final ServerInfo serverInfo;
|
||||||
|
|
||||||
|
protected List<ServerInfo.ResourceInfo>
|
||||||
public DavResourceFinder(Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void findResources(final ServerInfo serverInfo) throws URISyntaxException, IOException, HttpException, DavException {
|
|
||||||
final HttpClient httpClient = new HttpClient(context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);
|
|
||||||
|
|
||||||
// CardDAV
|
|
||||||
try {
|
|
||||||
Constants.log.info("*** CardDAV resource detection ***");
|
|
||||||
HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "carddav");
|
|
||||||
|
|
||||||
DavResource principal = new DavResource(httpClient, principalUrl);
|
|
||||||
principal.propfind(0, AddressbookHomeSet.NAME);
|
|
||||||
AddressbookHomeSet addrHomeSet = (AddressbookHomeSet) principal.properties.get(AddressbookHomeSet.NAME);
|
|
||||||
if (addrHomeSet != null && !addrHomeSet.hrefs.isEmpty()) {
|
|
||||||
Constants.log.info("Found addressbook home set(s): " + addrHomeSet);
|
|
||||||
|
|
||||||
// enumerate address books
|
|
||||||
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<>();
|
|
||||||
for (String href : addrHomeSet.hrefs) {
|
|
||||||
DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
|
|
||||||
homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME, AddressbookDescription.NAME);
|
|
||||||
for (DavResource member : homeSet.members) {
|
|
||||||
ResourceType type = (ResourceType) member.properties.get(ResourceType.NAME);
|
|
||||||
if (type != null && type.types.contains(ResourceType.ADDRESSBOOK)) {
|
|
||||||
Constants.log.info("Found address book: " + member.location);
|
|
||||||
|
|
||||||
CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet) member.properties.get(CurrentUserPrivilegeSet.NAME);
|
|
||||||
if (privs != null && (!privs.mayRead || !privs.mayWriteContent)) {
|
|
||||||
Constants.log.info("Only read/write address books are supported, ignoring this one");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayName displayName = (DisplayName) member.properties.get(DisplayName.NAME);
|
|
||||||
AddressbookDescription description = (AddressbookDescription) member.properties.get(AddressbookDescription.NAME);
|
|
||||||
|
|
||||||
addressBooks.add(new ServerInfo.ResourceInfo(
|
|
||||||
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
|
|
||||||
false,
|
|
||||||
member.location.toString(),
|
|
||||||
displayName != null ? displayName.displayName : null,
|
|
||||||
description != null ? description.description : null,
|
|
||||||
null
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serverInfo.setAddressBooks(addressBooks);
|
|
||||||
}
|
|
||||||
} catch(IOException|HttpException|DavException e) {
|
|
||||||
Constants.log.info("CardDAV detection failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalDAV
|
|
||||||
Constants.log.info("*** CalDAV resource detection ***");
|
|
||||||
try {
|
|
||||||
HttpUrl principalUrl = getCurrentUserPrincipal(httpClient, serverInfo, "caldav");
|
|
||||||
|
|
||||||
DavResource principal = new DavResource(httpClient, principalUrl);
|
|
||||||
principal.propfind(0, CalendarHomeSet.NAME);
|
|
||||||
CalendarHomeSet calHomeSet = (CalendarHomeSet) principal.properties.get(CalendarHomeSet.NAME);
|
|
||||||
if (calHomeSet != null && !calHomeSet.hrefs.isEmpty()) {
|
|
||||||
Constants.log.info("Found calendar home set(s): " + calHomeSet);
|
|
||||||
|
|
||||||
// enumerate address books
|
|
||||||
List<ServerInfo.ResourceInfo>
|
|
||||||
calendars = new LinkedList<>(),
|
calendars = new LinkedList<>(),
|
||||||
taskLists = new LinkedList<>();
|
taskLists = new LinkedList<>();
|
||||||
|
|
||||||
for (String href : calHomeSet.hrefs) {
|
|
||||||
DavResource homeSet = new DavResource(httpClient, principalUrl.resolve(href));
|
|
||||||
homeSet.propfind(1, ResourceType.NAME, CurrentUserPrivilegeSet.NAME, DisplayName.NAME,
|
|
||||||
CalendarDescription.NAME, CalendarColor.NAME, CalendarTimezone.NAME, SupportedCalendarComponentSet.NAME);
|
|
||||||
for (DavResource member : homeSet.members) {
|
|
||||||
ResourceType type = (ResourceType) member.properties.get(ResourceType.NAME);
|
|
||||||
if (type != null && type.types.contains(ResourceType.CALENDAR)) {
|
|
||||||
Constants.log.info("Found calendar: " + member.location);
|
|
||||||
|
|
||||||
DisplayName displayName = (DisplayName) member.properties.get(DisplayName.NAME);
|
public DavResourceFinder(Context context, ServerInfo serverInfo) {
|
||||||
CalendarDescription description = (CalendarDescription) member.properties.get(CalendarDescription.NAME);
|
this.context = context;
|
||||||
CalendarColor color = (CalendarColor) member.properties.get(CalendarColor.NAME);
|
this.serverInfo = serverInfo;
|
||||||
|
|
||||||
CurrentUserPrivilegeSet privs = (CurrentUserPrivilegeSet) member.properties.get(CurrentUserPrivilegeSet.NAME);
|
httpClient = new HttpClient(context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);
|
||||||
boolean readOnly = false;
|
|
||||||
if (privs != null) {
|
|
||||||
if (!privs.mayRead) {
|
|
||||||
Constants.log.info("Calendar not readable, ignoring this one");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
readOnly = !privs.mayWriteContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerInfo.ResourceInfo collection = new ServerInfo.ResourceInfo(
|
public void findResources() throws URISyntaxException, IOException, HttpException, DavException {
|
||||||
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
|
URI baseURI = serverInfo.getBaseURI();
|
||||||
readOnly,
|
String domain = null;
|
||||||
member.location.toString(),
|
|
||||||
displayName != null ? displayName.displayName : null,
|
HttpUrl principalUrl = null;
|
||||||
description != null ? description.description : null,
|
Set<HttpUrl> calendarHomeSets = new HashSet<>();
|
||||||
color != null ? color.color : null
|
|
||||||
|
if ("http".equals(baseURI.getScheme()) || "https".equals(baseURI.getScheme())) {
|
||||||
|
HttpUrl userURL = HttpUrl.get(baseURI);
|
||||||
|
|
||||||
|
/* check whether:
|
||||||
|
1. user-given URL is a calendar
|
||||||
|
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);
|
||||||
|
try {
|
||||||
|
davBase.propfind(0,
|
||||||
|
CalendarHomeSet.NAME, SupportedCalendarComponentSet.NAME,
|
||||||
|
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME,
|
||||||
|
CurrentUserPrincipal.NAME
|
||||||
);
|
);
|
||||||
|
addIfCalendar(davBase);
|
||||||
CalendarTimezone tz = (CalendarTimezone) member.properties.get(CalendarTimezone.NAME);
|
} catch (IOException | HttpException | DavException e) {
|
||||||
if (tz != null)
|
Constants.log.debug("PROPFIND on user-given URL failed", e);
|
||||||
collection.timezone = tz.vTimeZone;
|
|
||||||
|
|
||||||
boolean isCalendar = true, isTaskList = true;
|
|
||||||
SupportedCalendarComponentSet comp = (SupportedCalendarComponentSet) member.properties.get(SupportedCalendarComponentSet.NAME);
|
|
||||||
if (comp != null) {
|
|
||||||
isCalendar = comp.supportsEvents;
|
|
||||||
isTaskList = comp.supportsTasks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCalendar)
|
CalendarHomeSet calendarHomeSet = (CalendarHomeSet) davBase.properties.get(CalendarHomeSet.NAME);
|
||||||
calendars.add(collection);
|
if (calendarHomeSet != null) {
|
||||||
if (isTaskList)
|
Constants.log.info("Found <calendar-home-set> at user-given URL");
|
||||||
taskLists.add(collection);
|
for (String href : calendarHomeSet.hrefs) {
|
||||||
|
HttpUrl url = userURL.resolve(href);
|
||||||
|
if (url != null)
|
||||||
|
calendarHomeSets.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When home sets haven already been found, skip further searching.
|
||||||
|
* Otherwise (no home sets found), treat the user-given URL as "initial context path" for service discovery. */
|
||||||
|
if (calendarHomeSets.isEmpty()) {
|
||||||
|
Constants.log.info("No <calendar-home-set> set found, looking for <current-user-principal>");
|
||||||
|
|
||||||
|
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/caldav");
|
||||||
|
try {
|
||||||
|
principalUrl = getCurrentUserPrincipal(userURL.resolve("/.well-known/caldav"));
|
||||||
|
} catch (IOException|HttpException|DavException e) {
|
||||||
|
Constants.log.debug("PROPFIND on /.well-known/caldav failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try service discovery with "domain" = user-given host name
|
||||||
|
domain = baseURI.getHost();
|
||||||
|
} else if ("mailto".equals(baseURI.getScheme())) {
|
||||||
|
String mailbox = baseURI.getSchemeSpecificPart();
|
||||||
|
|
||||||
|
// determine service FQDN
|
||||||
|
int posAt = mailbox.lastIndexOf("@");
|
||||||
|
if (posAt == -1)
|
||||||
|
throw new URISyntaxException(mailbox, "Missing @ sign");
|
||||||
|
|
||||||
|
domain = mailbox.substring(posAt + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principalUrl == null && domain != null) {
|
||||||
|
Constants.log.info("No principal URL yet, trying SRV/TXT records with domain " + domain);
|
||||||
|
principalUrl = discoverPrincipalUrl(domain, "caldavs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// principal URL has been found, get calendar-home-set
|
||||||
|
if (principalUrl != null) {
|
||||||
|
Constants.log.info("Principal URL=" + principalUrl + ", getting <calendar-home-set>");
|
||||||
|
try {
|
||||||
|
DavResource principal = new DavResource(httpClient, principalUrl);
|
||||||
|
principal.propfind(0, CalendarHomeSet.NAME);
|
||||||
|
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)principal.properties.get(CalendarHomeSet.NAME);
|
||||||
|
if (calendarHomeSet != null)
|
||||||
|
Constants.log.info("Found <calendar-home-set> at principal URL");
|
||||||
|
for (String href : calendarHomeSet.hrefs) {
|
||||||
|
HttpUrl url = principal.location.resolve(href);
|
||||||
|
if (url != null)
|
||||||
|
calendarHomeSets.add(url);
|
||||||
|
}
|
||||||
|
} catch (IOException|HttpException|DavException e) {
|
||||||
|
Constants.log.debug("PROPFIND on " + principalUrl + " failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now query all home sets
|
||||||
|
for (HttpUrl url : calendarHomeSets)
|
||||||
|
try {
|
||||||
|
Constants.log.info("Listing collections in home set " + url);
|
||||||
|
DavResource homeSet = new DavResource(httpClient, url);
|
||||||
|
homeSet.propfind(1, SupportedCalendarComponentSet.NAME, ResourceType.NAME, DisplayName.NAME, CurrentUserPrivilegeSet.NAME,
|
||||||
|
CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME);
|
||||||
|
|
||||||
|
// home set should not be a calendar, but some servers have only one calendar and it's the home set
|
||||||
|
addIfCalendar(homeSet);
|
||||||
|
|
||||||
|
// members of the home set can be calendars, too
|
||||||
|
for (DavResource member : homeSet.members)
|
||||||
|
addIfCalendar(member);
|
||||||
|
} catch (IOException|HttpException|DavException e) {
|
||||||
|
Constants.log.debug("PROPFIND on " + url + " failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO CardDAV
|
||||||
|
|
||||||
|
// TODO remove duplicates
|
||||||
|
// TODO notify user on errors?
|
||||||
|
|
||||||
serverInfo.setCalendars(calendars);
|
serverInfo.setCalendars(calendars);
|
||||||
serverInfo.setTaskLists(taskLists);
|
serverInfo.setTaskLists(taskLists);
|
||||||
}
|
}
|
||||||
} catch(IOException|HttpException|DavException e) {
|
|
||||||
Constants.log.info("CalDAV detection failed", e);
|
/**
|
||||||
|
* If the given DavResource is a #{@link ResourceType#CALENDAR}:
|
||||||
|
* <ul>
|
||||||
|
* <li>add it to #{@link #calendars} if it supports VEVENT</li>
|
||||||
|
* <li>add it to #{@link #taskLists} if it supports VTODO</li>
|
||||||
|
* </ul>
|
||||||
|
* @param dav DavResource to check
|
||||||
|
*/
|
||||||
|
protected void addIfCalendar(@NonNull DavResource dav) {
|
||||||
|
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
|
||||||
|
if (resourceType != null && resourceType.types.contains(ResourceType.CALENDAR)) {
|
||||||
|
Constants.log.info("Found calendar collection at " + dav.location);
|
||||||
|
boolean supportsEvents = true, supportsTasks = true;
|
||||||
|
SupportedCalendarComponentSet supportedCalendarComponentSet = (SupportedCalendarComponentSet)dav.properties.get(SupportedCalendarComponentSet.NAME);
|
||||||
|
if (supportedCalendarComponentSet != null) {
|
||||||
|
supportsEvents = supportedCalendarComponentSet.supportsEvents;
|
||||||
|
supportsTasks = supportedCalendarComponentSet.supportsTasks;
|
||||||
|
}
|
||||||
|
if (supportsEvents)
|
||||||
|
calendars.add(resourceInfo(dav));
|
||||||
|
if (supportsTasks)
|
||||||
|
taskLists.add(resourceInfo(dav));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
/**
|
||||||
/*if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV())
|
* Builds a #{@link at.bitfire.davdroid.resource.ServerInfo.ResourceInfo} from a given
|
||||||
throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav));*/
|
* #{@link DavResource}. Uses the DAV properties current-user-properties, current-user-privilege-set,
|
||||||
|
* displayname, calendar-description and calendar-color. Make sure you have queried these
|
||||||
|
* properties from the DavResource.
|
||||||
|
* @param dav DavResource to take the resource info from
|
||||||
|
* @return ResourceInfo which represents the DavResource
|
||||||
|
*/
|
||||||
|
protected ServerInfo.ResourceInfo resourceInfo(DavResource dav) {
|
||||||
|
boolean readOnly = false;
|
||||||
|
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
|
||||||
|
if (privilegeSet != null)
|
||||||
|
readOnly = !privilegeSet.mayWriteContent;
|
||||||
|
|
||||||
|
String title = null;
|
||||||
|
DisplayName displayName = (DisplayName)dav.properties.get(DisplayName.NAME);
|
||||||
|
if (displayName != null)
|
||||||
|
title = displayName.displayName;
|
||||||
|
if (TextUtils.isEmpty(title))
|
||||||
|
title = UrlUtils.lastSegment(dav.location);
|
||||||
|
|
||||||
|
String description = null;
|
||||||
|
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
|
||||||
|
if (calendarDescription != null)
|
||||||
|
description = calendarDescription.description;
|
||||||
|
|
||||||
|
Integer color = null;
|
||||||
|
CalendarColor calendarColor = (CalendarColor)dav.properties.get(CalendarColor.NAME);
|
||||||
|
if (calendarColor != null)
|
||||||
|
color = calendarColor.color;
|
||||||
|
|
||||||
|
return new ServerInfo.ResourceInfo(
|
||||||
|
ServerInfo.ResourceInfo.Type.CALENDAR,
|
||||||
|
readOnly,
|
||||||
|
dav.location.toString(),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
color
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to find the principal URL by performing service discovery on a given domain name.
|
||||||
|
* @param domain domain name, e.g. "icloud.com"
|
||||||
|
* @param serviceName service name: "caldavs" or "carddavs"
|
||||||
|
* @return principal URL, or null if none found
|
||||||
|
*/
|
||||||
|
protected HttpUrl discoverPrincipalUrl(String domain, String serviceName) {
|
||||||
|
String scheme = null;
|
||||||
|
String fqdn = null;
|
||||||
|
Integer port = null;
|
||||||
|
String path = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final String query = "_" + serviceName + "._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 " + serviceName + " 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=")) {
|
||||||
|
path = segment.substring(5);
|
||||||
|
Constants.log.info("Found TXT record; initial context path=" + path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path == null) // no path from TXT records, use .well-known
|
||||||
|
path = "/.well-known/caldav";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(scheme) && !TextUtils.isEmpty(fqdn) && port != null && path != null) {
|
||||||
|
HttpUrl initialContextPath = new HttpUrl.Builder()
|
||||||
|
.scheme(scheme)
|
||||||
|
.host(fqdn).port(port)
|
||||||
|
.encodedPath(path)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpUrl principal = null;
|
||||||
|
try {
|
||||||
|
principal = getCurrentUserPrincipal(initialContextPath);
|
||||||
|
} catch(NotFoundException e) {
|
||||||
|
principal = getCurrentUserPrincipal(initialContextPath.resolve("/"));
|
||||||
|
}
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
} catch (IOException|HttpException|DavException e) {
|
||||||
|
Constants.log.debug("Service discovery failed", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries a given URL for current-user-principal
|
||||||
|
* @param url URL to query with PROPFIND (Depth: 0)
|
||||||
|
* @return current-user-principal URL, or null if none
|
||||||
|
*/
|
||||||
|
protected HttpUrl getCurrentUserPrincipal(HttpUrl url) throws IOException, HttpException, DavException {
|
||||||
|
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)
|
||||||
|
return url.resolve(currentUserPrincipal.href);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password)
|
* Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password)
|
||||||
@ -220,7 +364,7 @@ public class DavResourceFinder {
|
|||||||
|
|
||||||
// try to determine FQDN and port number using SRV records
|
// try to determine FQDN and port number using SRV records
|
||||||
try {
|
try {
|
||||||
String name = "_" + serviceName + "s._tcp." + domain;
|
String name = "_" + serviceName + "._tcp." + domain;
|
||||||
Constants.log.debug("Looking up SRV records for " + name);
|
Constants.log.debug("Looking up SRV records for " + name);
|
||||||
Record[] records = new Lookup(name, Type.SRV).run();
|
Record[] records = new Lookup(name, Type.SRV).run();
|
||||||
if (records != null && records.length >= 1) {
|
if (records != null && records.length >= 1) {
|
||||||
@ -229,7 +373,7 @@ public class DavResourceFinder {
|
|||||||
scheme = "https";
|
scheme = "https";
|
||||||
domain = srv.getTarget().toString(true);
|
domain = srv.getTarget().toString(true);
|
||||||
port = srv.getPort();
|
port = srv.getPort();
|
||||||
Log.d(TAG, "Found " + serviceName + "s service for " + domain + " -> " + domain + ":" + port);
|
Log.d(TAG, "Found " + serviceName + " service for " + domain + " -> " + domain + ":" + port);
|
||||||
|
|
||||||
// SRV record found, look for TXT record too (for initial context path)
|
// SRV record found, look for TXT record too (for initial context path)
|
||||||
records = new Lookup(name, Type.TXT).run();
|
records = new Lookup(name, Type.TXT).run();
|
||||||
@ -258,69 +402,11 @@ public class DavResourceFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
// helpers
|
||||||
* Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only
|
|
||||||
* if no current-user-principal can be detected for the .well-known location, the given location of the resource
|
|
||||||
* is tried.
|
|
||||||
* @param serverInfo Location that will be queried
|
|
||||||
* @param serviceName Well-known service name ("carddav", "caldav")
|
|
||||||
* @return WebDavResource of current-user-principal for the given service, or initial context URL if it can't be found
|
|
||||||
*
|
|
||||||
* TODO: If a TXT record is given, always use it instead of trying .well-known first
|
|
||||||
*/
|
|
||||||
HttpUrl getCurrentUserPrincipal(HttpClient httpClient, ServerInfo serverInfo, String serviceName) throws URISyntaxException {
|
|
||||||
HttpUrl initialURL = getInitialContextURL(serverInfo, serviceName);
|
|
||||||
|
|
||||||
if (initialURL != null) {
|
private SRVRecord selectSRVRecord(Record[] records) {
|
||||||
Constants.log.info("Looking up principal URL for service " + serviceName + "; initial context: " + initialURL);
|
|
||||||
|
|
||||||
// look for well-known service (RFC 5785)
|
|
||||||
try {
|
|
||||||
DavResource wellKnown = new DavResource(httpClient, initialURL.resolve("/.well-known/" + serviceName));
|
|
||||||
wellKnown.propfind(0, CurrentUserPrincipal.NAME);
|
|
||||||
|
|
||||||
CurrentUserPrincipal principal = (CurrentUserPrincipal)wellKnown.properties.get(CurrentUserPrincipal.NAME);
|
|
||||||
if (principal != null) {
|
|
||||||
HttpUrl url = wellKnown.location.resolve(principal.href);
|
|
||||||
Constants.log.info("Found principal URL from well-known URL: " + url);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
|
|
||||||
} catch (HttpException e) {
|
|
||||||
Constants.log.warn("Well-known " + serviceName + " service detection failed with HTTP error", e);
|
|
||||||
} catch (DavException e) {
|
|
||||||
Constants.log.warn("Well-known " + serviceName + " service detection failed with DAV error", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fall back to user-given initial context path
|
|
||||||
Log.d(TAG, "Well-known service detection failed, trying initial context path " + initialURL);
|
|
||||||
try {
|
|
||||||
DavResource base = new DavResource(httpClient, initialURL);
|
|
||||||
base.propfind(0, CurrentUserPrincipal.NAME);
|
|
||||||
CurrentUserPrincipal principal = (CurrentUserPrincipal)base.properties.get(CurrentUserPrincipal.NAME);
|
|
||||||
if (principal != null) {
|
|
||||||
HttpUrl url = base.location.resolve(principal.href);
|
|
||||||
Constants.log.info("Found principal URL from initial context URL: " + url);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Constants.log.warn("Well-known " + serviceName + " service detection failed with I/O error", e);
|
|
||||||
} catch (HttpException e) {
|
|
||||||
Log.e(TAG, "HTTP error when querying principal", e);
|
|
||||||
} catch (DavException e) {
|
|
||||||
Log.e(TAG, "DAV error when querying principal", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName + ". Assuming principal path is initial context path!");
|
|
||||||
return initialURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SRVRecord selectSRVRecord(Record[] records) {
|
|
||||||
if (records.length > 1)
|
if (records.length > 1)
|
||||||
Log.w(TAG, "Multiple SRV records not supported yet; using first one");
|
Constants.log.warn("Multiple SRV records not supported yet; using first one");
|
||||||
return (SRVRecord)records[0];
|
return (SRVRecord)records[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ public class ServerInfo implements Serializable {
|
|||||||
|
|
||||||
@RequiredArgsConstructor(suppressConstructorProperties=true)
|
@RequiredArgsConstructor(suppressConstructorProperties=true)
|
||||||
@Data
|
@Data
|
||||||
public static class ResourceInfo implements Serializable {
|
public static class ResourceInfo implements Cloneable, Serializable {
|
||||||
public enum Type {
|
public enum Type {
|
||||||
ADDRESS_BOOK,
|
ADDRESS_BOOK,
|
||||||
CALENDAR
|
CALENDAR
|
||||||
@ -64,7 +64,13 @@ public class ServerInfo implements Serializable {
|
|||||||
|
|
||||||
|
|
||||||
// copy constructor
|
// copy constructor
|
||||||
public ResourceInfo(ResourceInfo src) {
|
|
||||||
|
@Override
|
||||||
|
public ResourceInfo clone() {
|
||||||
|
return new ResourceInfo(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResourceInfo(ResourceInfo src) {
|
||||||
enabled = src.enabled;
|
enabled = src.enabled;
|
||||||
type = src.type;
|
type = src.type;
|
||||||
readOnly = src.readOnly;
|
readOnly = src.readOnly;
|
||||||
@ -77,7 +83,6 @@ public class ServerInfo implements Serializable {
|
|||||||
timezone = src.timezone;
|
timezone = src.timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// some logic
|
// some logic
|
||||||
|
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
|
@ -9,6 +9,7 @@ package at.bitfire.davdroid.syncadapter;
|
|||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
@ -77,8 +78,8 @@ public class AccountSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check whether Android version has changed
|
// check whether Android version has changed
|
||||||
int lastAndroidVersion = NumberUtils.toInt(accountManager.getUserData(account, KEY_LAST_ANDROID_VERSION));
|
String lastAndroidVersionInt = accountManager.getUserData(account, KEY_LAST_ANDROID_VERSION);
|
||||||
if (lastAndroidVersion < Build.VERSION.SDK_INT) {
|
if (lastAndroidVersionInt != null && NumberUtils.toInt(lastAndroidVersionInt) < Build.VERSION.SDK_INT) {
|
||||||
// notify user
|
// notify user
|
||||||
showNotification(Constants.NOTIFICATION_ANDROID_VERSION_UPDATED,
|
showNotification(Constants.NOTIFICATION_ANDROID_VERSION_UPDATED,
|
||||||
context.getString(R.string.settings_android_update_title),
|
context.getString(R.string.settings_android_update_title),
|
||||||
@ -89,6 +90,7 @@ public class AccountSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||||
protected void showNotification(int id, String title, String message) {
|
protected void showNotification(int id, String title, String message) {
|
||||||
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
Notification.Builder n = new Notification.Builder(context);
|
Notification.Builder n = new Notification.Builder(context);
|
||||||
|
@ -23,6 +23,7 @@ import com.squareup.okhttp.ResponseBody;
|
|||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -72,7 +73,10 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
// prepare local address book
|
// prepare local address book
|
||||||
localCollection = new LocalAddressBook(account, provider);
|
localCollection = new LocalAddressBook(account, provider);
|
||||||
|
|
||||||
collectionURL = HttpUrl.parse(localAddressBook().getURL());
|
String url = localAddressBook().getURL();
|
||||||
|
if (url == null)
|
||||||
|
throw new ContactsStorageException("Couldn't get address book URL");
|
||||||
|
collectionURL = HttpUrl.parse(url);
|
||||||
davCollection = new DavAddressBook(httpClient, collectionURL);
|
davCollection = new DavAddressBook(httpClient, collectionURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +105,16 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
@Override
|
@Override
|
||||||
protected void listRemote() throws IOException, HttpException, DavException {
|
protected void listRemote() throws IOException, HttpException, DavException {
|
||||||
// fetch list of remote VCards and build hash table to index file name
|
// fetch list of remote VCards and build hash table to index file name
|
||||||
|
|
||||||
|
try {
|
||||||
davAddressBook().addressbookQuery();
|
davAddressBook().addressbookQuery();
|
||||||
|
} catch(HttpException e) {
|
||||||
|
if (e.status/100 == 4) {
|
||||||
|
Constants.log.warn("Server error on REPORT addressbook query, falling back to PROPFIND", e);
|
||||||
|
davAddressBook().propfind(1, GetETag.NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
remoteResources = new HashMap<>(davCollection.members.size());
|
remoteResources = new HashMap<>(davCollection.members.size());
|
||||||
for (DavResource vCard : davCollection.members) {
|
for (DavResource vCard : davCollection.members) {
|
||||||
String fileName = vCard.fileName();
|
String fileName = vCard.fileName();
|
||||||
|
@ -202,7 +202,7 @@ abstract public class SyncManager {
|
|||||||
Notification notification;
|
Notification notification;
|
||||||
builder .setSmallIcon(R.drawable.ic_launcher)
|
builder .setSmallIcon(R.drawable.ic_launcher)
|
||||||
.setContentTitle(context.getString(R.string.sync_error_title, account.name))
|
.setContentTitle(context.getString(R.string.sync_error_title, account.name))
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
|
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 20)
|
if (Build.VERSION.SDK_INT >= 20)
|
||||||
builder.setLocalOnly(true);
|
builder.setLocalOnly(true);
|
||||||
|
@ -60,7 +60,7 @@ public class TasksSyncAdapterService extends Service {
|
|||||||
throw new CalendarStorageException("Couldn't access OpenTasks provider");
|
throw new CalendarStorageException("Couldn't access OpenTasks provider");
|
||||||
|
|
||||||
for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null)) {
|
for (LocalTaskList taskList : (LocalTaskList[])LocalTaskList.find(account, provider, LocalTaskList.Factory.INSTANCE, null, null)) {
|
||||||
Constants.log.info("Synchronizing task list #" + taskList.getId() + ", URL: " + taskList.getName());
|
Constants.log.info("Synchronizing task list #" + taskList.getId() + ", URL: " + taskList.getSyncId());
|
||||||
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, extras, provider, syncResult, taskList);
|
TasksSyncManager syncManager = new TasksSyncManager(getContext(), account, extras, provider, syncResult, taskList);
|
||||||
syncManager.performSync();
|
syncManager.performSync();
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ public class TasksSyncManager extends SyncManager {
|
|||||||
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
|
int color = (pColor != null && pColor.color != null) ? pColor.color : LocalCalendar.defaultColor;
|
||||||
|
|
||||||
ContentValues values = new ContentValues(2);
|
ContentValues values = new ContentValues(2);
|
||||||
Constants.log.info("Setting new calendar name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
|
Constants.log.info("Setting new task list name \"" + displayName + "\" and color 0x" + Integer.toHexString(color));
|
||||||
values.put(TaskLists.LIST_NAME, displayName);
|
values.put(TaskLists.LIST_NAME, displayName);
|
||||||
values.put(TaskLists.LIST_COLOR, color);
|
values.put(TaskLists.LIST_COLOR, color);
|
||||||
localTaskList().update(values);
|
localTaskList().update(values);
|
||||||
|
@ -103,6 +103,7 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
|||||||
@Override
|
@Override
|
||||||
public void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws ContactsStorageException {
|
public void createLocalCollection(Account account, ServerInfo.ResourceInfo resource) throws ContactsStorageException {
|
||||||
@Cleanup("release") ContentProviderClient provider = getActivity().getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
@Cleanup("release") ContentProviderClient provider = getActivity().getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY);
|
||||||
|
if (provider != null) {
|
||||||
LocalAddressBook addressBook = new LocalAddressBook(account, provider);
|
LocalAddressBook addressBook = new LocalAddressBook(account, provider);
|
||||||
|
|
||||||
// set URL
|
// set URL
|
||||||
@ -113,6 +114,8 @@ public class AccountDetailsFragment extends Fragment implements TextWatcher {
|
|||||||
settings.put(ContactsContract.Settings.SHOULD_SYNC, 1);
|
settings.put(ContactsContract.Settings.SHOULD_SYNC, 1);
|
||||||
settings.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
settings.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
|
||||||
addressBook.updateSettings(settings);
|
addressBook.updateSettings(settings);
|
||||||
|
} else
|
||||||
|
Constants.log.error("Couldn't access Contacts Provider");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -110,8 +110,8 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DavResourceFinder finder = new DavResourceFinder(context);
|
DavResourceFinder finder = new DavResourceFinder(context, serverInfo);
|
||||||
finder.findResources(serverInfo);
|
finder.findResources();
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage()));
|
serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -165,10 +165,10 @@ public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter
|
|||||||
setContent((CheckedTextView)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position));
|
setContent((CheckedTextView)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position));
|
||||||
break;
|
break;
|
||||||
case TYPE_CALENDARS_ROW:
|
case TYPE_CALENDARS_ROW:
|
||||||
setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo) getItem(position));
|
setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position));
|
||||||
break;
|
break;
|
||||||
case TYPE_TASK_LISTS_ROW:
|
case TYPE_TASK_LISTS_ROW:
|
||||||
setContent((CheckedTextView)v, R.drawable.tasks, (ServerInfo.ResourceInfo) getItem(position));
|
setContent((CheckedTextView)v, R.drawable.tasks, (ServerInfo.ResourceInfo)getItem(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable task list selection if there's no local task provider
|
// disable task list selection if there's no local task provider
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 429648cfae534dd0476f813c94ab1cae1f140223
|
Subproject commit b1ca2ff7383381a407baffd05a6b3532e4cd1690
|
Loading…
Reference in New Issue
Block a user