1
0
mirror of https://github.com/etesync/android synced 2025-03-25 03:45:46 +00:00

New initial server configuration detection

* separate initial server configuration (= principal and/or a certain collection) detection from collection refresh (to be done)
* GUI: LoginActivity
This commit is contained in:
Ricki Hirner 2016-01-16 00:53:05 +01:00
parent 515969c4b8
commit ba0350c83d
29 changed files with 602 additions and 715 deletions

View File

@ -32,6 +32,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
disable 'IconColors'
disable 'IconLauncherShape'

View File

@ -112,7 +112,7 @@
</intent-filter>
</activity>
<activity
android:name=".ui.AddAccountActivity"
android:name=".ui.setup.LoginActivity"
android:label="@string/login_title"
android:parentActivityName=".ui.AccountsActivity"
android:noHistory="true">

View File

@ -13,6 +13,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Constants {
public static final String
ACCOUNT_TYPE = "bitfire.at.davdroid";
@ -26,5 +27,6 @@ public class Constants {
NOTIFICATION_CALENDAR_SYNC = 11,
NOTIFICATION_TASK_SYNC = 12;
public final static Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
public static final Uri webUri = Uri.parse("https://davdroid.bitfire.at/?pk_campaign=davdroid-app");
}

View File

@ -11,7 +11,6 @@ import android.content.Context;
import android.text.TextUtils;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.OkHttpClient;
import org.slf4j.Logger;
import org.xbill.DNS.Lookup;
@ -22,14 +21,10 @@ import org.xbill.DNS.Type;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.UrlUtils;
@ -47,9 +42,12 @@ import at.bitfire.dav4android.property.CurrentUserPrivilegeSet;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.dav4android.property.SupportedCalendarComponentSet;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.log.StringLogger;
import at.bitfire.davdroid.ui.setup.LoginCredentialsFragment;
import lombok.Data;
import lombok.NonNull;
import lombok.ToString;
public class DavResourceFinder {
protected enum Service {
@ -64,245 +62,140 @@ public class DavResourceFinder {
protected final Logger log;
protected final Context context;
protected final HttpClient httpClient;
protected final ServerInfo serverInfo;
protected final LoginCredentialsFragment.LoginCredentials credentials;
protected Map<HttpUrl, ServerInfo.ResourceInfo>
addressbooks = new HashMap<>(),
calendars = new HashMap<>(),
taskLists = new HashMap<>();
protected HttpUrl carddavPrincipal, caldavPrincipal;
protected Map<HttpUrl, ServerConfiguration.Collection>
addressBooks = new HashMap<>(),
calendars = new HashMap<>();
public DavResourceFinder(Logger log, Context context, ServerInfo serverInfo) {
this.log = log;
public DavResourceFinder(Context context, LoginCredentialsFragment.LoginCredentials credentials) {
this.context = context;
this.serverInfo = serverInfo;
this.credentials = credentials;
httpClient = new HttpClient(log, context, serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.authPreemptive);
log = new StringLogger("DavResourceFinder", true);
httpClient = new HttpClient(log, context, credentials.getUserName(), credentials.getPassword(), credentials.isAuthPreemptive());
}
public void findResources() {
try {
findResources(Service.CARDDAV);
findResources(Service.CALDAV);
} catch(URISyntaxException e) {
log.warn("Invalid user-given URI", e);
}
public ServerConfiguration findInitialConfiguration() {
addressBooks.clear();
findInitialConfiguration(Service.CARDDAV);
calendars.clear();
findInitialConfiguration(Service.CALDAV);
return new ServerConfiguration(
carddavPrincipal, addressBooks.values().toArray(new ServerConfiguration.Collection[0]),
caldavPrincipal, calendars.values().toArray(new ServerConfiguration.Collection[0]),
log.toString()
);
}
public void findResources(Service service) throws URISyntaxException {
URI baseURI = serverInfo.getBaseURI();
protected void findInitialConfiguration(Service service) {
// user-given base URI (mailto or URL)
URI baseURI = credentials.getUri();
// domain for service discovery
String domain = null;
HttpUrl principalUrl = null;
Set<HttpUrl> homeSets = new HashSet<>();
HttpUrl principal = null;
if (service == Service.CALDAV) {
calendars.clear();
taskLists.clear();
} else if (service == Service.CARDDAV)
addressbooks.clear();
// Step 1a (only when user-given URI is URL):
// * Check whether URL represents a calendar/address-book collection itself,
// * and/or whether it has a current-user-principal,
// * or whether it represents a principal itself.
if ("http".equalsIgnoreCase(baseURI.getScheme()) || "https".equalsIgnoreCase(baseURI.getScheme())) {
HttpUrl baseURL = HttpUrl.get(baseURI);
log.info("*** STARTING COLLECTION DISCOVERY FOR SERVICE " + service.name.toUpperCase(Locale.US) + "***");
if ("http".equals(baseURI.getScheme()) || "https".equals(baseURI.getScheme())) {
HttpUrl userURL = HttpUrl.get(baseURI);
// remember domain for service discovery (if required)
// try service discovery only for https:// URLs because only secure service discovery is implemented
if ("https".equalsIgnoreCase(baseURL.scheme()))
domain = baseURI.getHost();
/* 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)
*/
log.info("Check whether user-given URL is a calendar collection and/or contains home-set and/or has current-user-principal");
DavResource davBase = new DavResource(log, httpClient, userURL);
log.info("Checking user-given URL: " + baseURL.toString());
try {
if (service == Service.CALDAV) {
davBase.propfind(0,
CalendarHomeSet.NAME, SupportedCalendarComponentSet.NAME,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME
);
addIfCalendar(davBase);
} else if (service == Service.CARDDAV) {
DavResource davBase = new DavResource(log, httpClient, baseURL);
if (service == Service.CARDDAV) {
davBase.propfind(0,
AddressbookHomeSet.NAME,
ResourceType.NAME, DisplayName.NAME, AddressbookDescription.NAME, CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME
);
addIfAddressBook(davBase);
} else if (service == Service.CALDAV) {
davBase.propfind(0,
CalendarHomeSet.NAME, SupportedCalendarComponentSet.NAME,
ResourceType.NAME, DisplayName.NAME, CalendarColor.NAME, CalendarDescription.NAME, CalendarTimezone.NAME, CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME
);
addIfCalendar(davBase);
}
// check for current-user-principal
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principal = davBase.location.resolve(currentUserPrincipal.href);
// check for resourcetype = principal
if (principal == null) {
ResourceType resourceType = (ResourceType)davBase.properties.get(ResourceType.NAME);
if (resourceType.types.contains(ResourceType.PRINCIPAL))
principal = davBase.location;
}
// If a principal has been detected successfully, ensure that it provides the required service.
if (principal != null && !providesService(principal, service))
principal = null;
} catch (IOException|HttpException|DavException e) {
log.debug("PROPFIND on user-given URL failed", e);
}
if (service == Service.CALDAV) {
CalendarHomeSet calendarHomeSet = (CalendarHomeSet)davBase.properties.get(CalendarHomeSet.NAME);
if (calendarHomeSet != null) {
log.info("Found <calendar-home-set> at user-given URL");
for (String href : calendarHomeSet.hrefs) {
HttpUrl url = userURL.resolve(href);
if (url != null)
homeSets.add(url);
}
}
} else if (service == Service.CARDDAV) {
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet) davBase.properties.get(AddressbookHomeSet.NAME);
if (addressbookHomeSet != null) {
log.info("Found <addressbook-home-set> at user-given URL");
for (String href : addressbookHomeSet.hrefs) {
HttpUrl url = userURL.resolve(href);
if (url != null)
homeSets.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.
*
* Keep in mind that the CalDAV principal URL must not be the CardDAV principal URL! */
if (homeSets.isEmpty())
// Step 1b: Try well-known URL, too
if (principal == null)
try {
log.info("No home sets found, looking for <current-user-principal>");
davBase.options();
if ((service == Service.CALDAV && davBase.capabilities.contains("calendar-access")) ||
(service == Service.CARDDAV && davBase.capabilities.contains("addressbook"))) {
CurrentUserPrincipal currentUserPrincipal = (CurrentUserPrincipal)davBase.properties.get(CurrentUserPrincipal.NAME);
if (currentUserPrincipal != null && currentUserPrincipal.href != null)
principalUrl = davBase.location.resolve(currentUserPrincipal.href);
}
} catch(IOException|HttpException|DavException e) {
log.debug("Couldn't find <current-user-principal> at user-given URL", e);
principal = getCurrentUserPrincipal(baseURL.resolve("/.well-known/" + service.name), service);
} catch (IOException|HttpException|DavException e) {
log.debug("Well-known URL detection failed", e);
}
if (principalUrl == null)
try {
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) {
log.debug("Couldn't determine <current-user-principal> from well-known " + service + " path", e);
}
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())) {
} else if ("mailto".equalsIgnoreCase(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 (posAt != -1)
domain = mailbox.substring(posAt + 1);
}
if (principalUrl == null && domain != null) {
log.info("No principal URL yet, trying SRV/TXT records with domain " + domain);
// Step 2: If user-given URL didn't reveal a principal, search for it: SERVICE DISCOVERY
if (principal == null && domain != null) {
log.info("No principal found at user-given URL, trying to discover");
try {
principalUrl = discoverPrincipalUrl(domain, service);
principal = discoverPrincipalUrl(domain, service);
} catch (IOException|HttpException|DavException e) {
log.info("Couldn't find principal URL using service discovery");
log.debug(service.name + " service discovery failed", e);
}
}
// principal URL has been found, get addressbook-home-set/calendar-home-set
if (principalUrl != null) {
log.info("Principal URL=" + principalUrl + ", getting <calendar-home-set>");
try {
DavResource principal = new DavResource(log, httpClient, principalUrl);
if (service == Service.CALDAV) {
principal.propfind(0, CalendarHomeSet.NAME);
CalendarHomeSet calendarHomeSet = (CalendarHomeSet) principal.properties.get(CalendarHomeSet.NAME);
if (calendarHomeSet != null) {
log.info("Found <calendar-home-set> at principal URL");
for (String href : calendarHomeSet.hrefs) {
HttpUrl url = principal.location.resolve(href);
if (url != null)
homeSets.add(url);
}
}
} else if (service == Service.CARDDAV) {
principal.propfind(0, AddressbookHomeSet.NAME);
AddressbookHomeSet addressbookHomeSet = (AddressbookHomeSet) principal.properties.get(AddressbookHomeSet.NAME);
if (addressbookHomeSet != null) {
log.info("Found <addressbook-home-set> at principal URL");
for (String href : addressbookHomeSet.hrefs) {
HttpUrl url = principal.location.resolve(href);
if (url != null)
homeSets.add(url);
}
}
}
} catch (IOException|HttpException|DavException e) {
log.debug("PROPFIND on " + principalUrl + " failed", e);
}
}
// now query all home sets
for (HttpUrl url : homeSets)
if (service == Service.CALDAV)
try {
log.info("Listing calendar collections in home set " + 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);
// 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) {
log.debug("PROPFIND on " + url + " failed", e);
}
else if (service == Service.CARDDAV)
try {
log.info("Listing address books in home set " + 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
addIfAddressBook(homeSet);
// members of the home set can be calendars, too
for (DavResource member : homeSet.members)
addIfAddressBook(member);
} catch (IOException | HttpException | DavException e) {
log.debug("PROPFIND on " + url + " failed", e);
}
if (service == Service.CALDAV) {
serverInfo.setCalendars(calendars.values().toArray(new ServerInfo.ResourceInfo[calendars.size()]));
serverInfo.setTaskLists(taskLists.values().toArray(new ServerInfo.ResourceInfo[taskLists.size()]));
} else if (service == Service.CARDDAV)
serverInfo.setAddressBooks(addressbooks.values().toArray(new ServerInfo.ResourceInfo[addressbooks.size()]));
if (service == Service.CALDAV)
caldavPrincipal = principal;
else if (service == Service.CARDDAV)
carddavPrincipal = principal;
}
/**
* If the given DavResource is a #{@link ResourceType#ADDRESSBOOK}, add it to #{@link #addressbooks}.
* @param dav DavResource to check
*/
protected void addIfAddressBook(@NonNull DavResource dav) {
ResourceType resourceType = (ResourceType)dav.properties.get(ResourceType.NAME);
if (resourceType != null && resourceType.types.contains(ResourceType.ADDRESSBOOK)) {
dav.location = UrlUtils.withTrailingSlash(dav.location);
log.info("Found address book at " + dav.location);
addressbooks.put(dav.location, resourceInfo(dav, ServerInfo.ResourceInfo.Type.ADDRESS_BOOK));
addressBooks.put(dav.location, collectionInfo(dav, ServerConfiguration.Collection.Type.ADDRESS_BOOK));
}
}
/**
* 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)) {
@ -315,10 +208,12 @@ public class DavResourceFinder {
supportsEvents = supportedCalendarComponentSet.supportsEvents;
supportsTasks = supportedCalendarComponentSet.supportsTasks;
}
if (supportsEvents)
calendars.put(dav.location, resourceInfo(dav, ServerInfo.ResourceInfo.Type.CALENDAR));
if (supportsTasks)
taskLists.put(dav.location, resourceInfo(dav, ServerInfo.ResourceInfo.Type.CALENDAR));
if (supportsEvents || supportsTasks) {
ServerConfiguration.Collection info = collectionInfo(dav, ServerConfiguration.Collection.Type.CALENDAR);
info.supportsEvents = supportsEvents;
info.supportsTasks = supportsTasks;
calendars.put(dav.location, info);
}
}
}
@ -333,7 +228,7 @@ public class DavResourceFinder {
* @param type must be ADDRESS_BOOK or CALENDAR
* @return ResourceInfo which represents the DavResource
*/
protected ServerInfo.ResourceInfo resourceInfo(DavResource dav, ServerInfo.ResourceInfo.Type type) {
protected ServerConfiguration.Collection collectionInfo(DavResource dav, ServerConfiguration.Collection.Type type) {
boolean readOnly = false;
CurrentUserPrivilegeSet privilegeSet = (CurrentUserPrivilegeSet)dav.properties.get(CurrentUserPrivilegeSet.NAME);
if (privilegeSet != null)
@ -348,11 +243,11 @@ public class DavResourceFinder {
String description = null;
Integer color = null;
if (type == ServerInfo.ResourceInfo.Type.ADDRESS_BOOK) {
if (type == ServerConfiguration.Collection.Type.ADDRESS_BOOK) {
AddressbookDescription addressbookDescription = (AddressbookDescription)dav.properties.get(AddressbookDescription.NAME);
if (addressbookDescription != null)
description = addressbookDescription.description;
} else if (type == ServerInfo.ResourceInfo.Type.CALENDAR) {
} else if (type == ServerConfiguration.Collection.Type.CALENDAR) {
CalendarDescription calendarDescription = (CalendarDescription)dav.properties.get(CalendarDescription.NAME);
if (calendarDescription != null)
description = calendarDescription.description;
@ -362,7 +257,7 @@ public class DavResourceFinder {
color = calendarColor.color;
}
return new ServerInfo.ResourceInfo(
ServerConfiguration.Collection collection = new ServerConfiguration.Collection(
type,
readOnly,
UrlUtils.withTrailingSlash(dav.location).toString(),
@ -370,10 +265,30 @@ public class DavResourceFinder {
description,
color
);
return collection;
}
boolean providesService(HttpUrl url, Service service) {
DavResource davPrincipal = new DavResource(log, httpClient, url);
try {
davPrincipal.options();
if ((service == Service.CARDDAV && davPrincipal.capabilities.contains("addressbook")) ||
(service == Service.CALDAV && davPrincipal.capabilities.contains("calendar-access")))
return true;
} catch (IOException|HttpException|DavException e) {
log.error("Couldn't detect services on {}", url);
}
return false;
}
/**
* Try to find the principal URL by performing service discovery on a given domain name.
* Only secure services (caldavs, carddavs) will be discovered!
* @param domain domain name, e.g. "icloud.com"
* @param service service to discover (CALDAV or CARDDAV)
* @return principal URL, or null if none found
@ -381,7 +296,7 @@ public class DavResourceFinder {
protected HttpUrl discoverPrincipalUrl(String domain, Service service) throws IOException, HttpException, DavException {
String scheme = null;
String fqdn = null;
Integer port = null;
Integer port = 443;
List<String> paths = new LinkedList<>(); // there may be multiple paths to try
final String query = "_" + service.name + "s._tcp." + domain;
@ -394,29 +309,36 @@ public class DavResourceFinder {
scheme = "https";
fqdn = srv.getTarget().toString(true);
port = srv.getPort();
log.info("Found " + service + " service: fqdn=" + fqdn + ", port=" + port);
log.info("Found " + service + " service at https://" + fqdn + ":" + port);
// look for TXT record too (for initial context path)
records = new Lookup(query, Type.TXT).run();
if (records != null)
for (Record record : records)
if (record instanceof TXTRecord)
for (String segment : (List<String>) ((TXTRecord) record).getStrings())
if (segment.startsWith("path=")) {
paths.add(segment.substring(5));
log.info("Found TXT record; initial context path=" + paths);
break;
}
} else {
// no SRV records, try domain name as FQDN
log.info("Didn't find " + service + " service, trying at https://" + domain + ":" + port);
// if there's TXT record and if it it's wrong, try well-known
paths.add("/.well-known/" + service.name);
// if this fails, too, try "/"
paths.add("/");
scheme = "https";
fqdn = domain;
}
// look for TXT record too (for initial context path)
records = new Lookup(query, Type.TXT).run();
if (records != null)
for (Record record : records)
if (record instanceof TXTRecord)
for (String segment : (List<String>) ((TXTRecord) record).getStrings())
if (segment.startsWith("path=")) {
paths.add(segment.substring(5));
log.info("Found TXT record; initial context path=" + paths);
break;
}
// if there's TXT record and 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)
try {
if (!TextUtils.isEmpty(scheme) && !TextUtils.isEmpty(fqdn) && port != null && paths != null) {
if (!TextUtils.isEmpty(scheme) && !TextUtils.isEmpty(fqdn) && paths != null) {
HttpUrl initialContextPath = new HttpUrl.Builder()
.scheme(scheme)
.host(fqdn).port(port)
@ -424,7 +346,8 @@ public class DavResourceFinder {
.build();
log.info("Trying to determine principal from initial context path=" + initialContextPath);
HttpUrl principal = getCurrentUserPrincipal(initialContextPath);
HttpUrl principal = getCurrentUserPrincipal(initialContextPath, service);
if (principal != null)
return principal;
}
@ -436,17 +359,25 @@ public class DavResourceFinder {
/**
* 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
* @param url URL to query with PROPFIND (Depth: 0)
* @param service required service (may be null, in which case no service check is done)
* @return current-user-principal URL that provides required service, or null if none
*/
protected HttpUrl getCurrentUserPrincipal(HttpUrl url) throws IOException, HttpException, DavException {
protected HttpUrl getCurrentUserPrincipal(HttpUrl url, Service service) 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);
HttpUrl principal = dav.location.resolve(currentUserPrincipal.href);
if (principal != null) {
log.info("Found current-user-principal: " + principal);
// service check
if (service != null && !providesService(principal, service)) {
log.info("{} doesn't provide required {} service, dismissing", principal, service);
principal = null;
}
return principal;
}
}
@ -462,4 +393,42 @@ public class DavResourceFinder {
return (SRVRecord)records[0];
}
// data classes
@Data
@ToString(exclude="logs")
public static class ServerConfiguration {
final public HttpUrl cardDavPrincipal;
final public Collection[] addressBooks;
final public HttpUrl calDavPrincipal;
final public Collection[] calendars;
final String logs;
@Data
@ToString
public static class Collection {
public enum Type {
ADDRESS_BOOK,
CALENDAR
}
final Type type;
final boolean readOnly;
final String url, // absolute URL of resource
title,
description;
final Integer color;
/**
* full VTIMEZONE definition (not the TZ ID)
*/
boolean supportsEvents, supportsTasks;
String timezone;
}
}
}

View File

@ -18,7 +18,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import at.bitfire.davdroid.ui.AddAccountActivity;
import at.bitfire.davdroid.ui.setup.LoginActivity;
public class AccountAuthenticatorService extends Service {
private static AccountAuthenticator accountAuthenticator;
@ -48,7 +48,7 @@ public class AccountAuthenticatorService extends Service {
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType,
String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Intent intent = new Intent(context, AddAccountActivity.class);
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);

View File

@ -22,6 +22,7 @@ import android.view.View;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.setup.LoginActivity;
public class AccountsActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
@ -37,7 +38,7 @@ public class AccountsActivity extends AppCompatActivity implements NavigationVie
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(AccountsActivity.this, AddAccountActivity.class));
startActivity(new Intent(AccountsActivity.this, LoginActivity.class));
}
});

View File

@ -10,6 +10,7 @@ package at.bitfire.davdroid.ui;
import android.content.Context;
import android.support.v7.widget.AppCompatCheckBox;
import android.text.Editable;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.widget.CompoundButton;
@ -19,6 +20,9 @@ import android.widget.LinearLayout;
import at.bitfire.davdroid.R;
public class EditPassword extends LinearLayout {
private static final String NS_ANDROID = "http://schemas.android.com/apk/res/android";
EditText editPassword;
public EditPassword(Context context) {
super(context, null);
@ -29,7 +33,9 @@ public class EditPassword extends LinearLayout {
inflate(context, R.layout.edit_password, this);
final EditText editPassword = (EditText)findViewById(R.id.password);
editPassword = (EditText)findViewById(R.id.password);
editPassword.setHint(attrs.getAttributeResourceValue(NS_ANDROID, "hint", 0));
editPassword.setText(attrs.getAttributeValue(NS_ANDROID, "text"));
AppCompatCheckBox checkShowPassword = (AppCompatCheckBox)findViewById(R.id.show_password);
checkShowPassword.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@ -42,4 +48,16 @@ public class EditPassword extends LinearLayout {
});
}
public Editable getText() {
return editPassword.getText();
}
public void setError(CharSequence error) {
editPassword.setError(error);
}
public void setText(CharSequence text) {
editPassword.setText(text);
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright © 2013 2016 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.ui.setup;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.DavResourceFinder;
import at.bitfire.davdroid.resource.DavResourceFinder.ServerConfiguration;
import lombok.Cleanup;
public class DetectConfigurationFragment extends DialogFragment implements LoaderManager.LoaderCallbacks<ServerConfiguration> {
static final String ARG_LOGIN_CREDENTIALS = "credentials";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog dialog = new ProgressDialog(getActivity());
dialog.setCanceledOnTouchOutside(false);
setCancelable(false);
dialog.setTitle(R.string.login_configuration_detection);
dialog.setIndeterminate(true);
dialog.setMessage(getString(R.string.login_querying_server));
return dialog;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(0, getArguments(), this);
}
@Override
public Loader<ServerConfiguration> onCreateLoader(int id, Bundle args) {
return new ServerConfigurationLoader(getContext(), args);
}
@Override
public void onLoadFinished(Loader<ServerConfiguration> loader, ServerConfiguration data) {
// show error / continue with next fragment
Constants.log.info("detection results: {}", data);
dismissAllowingStateLoss();
}
@Override
public void onLoaderReset(Loader<ServerConfiguration> loader) {
}
static class ServerConfigurationLoader extends AsyncTaskLoader<ServerConfiguration> {
final Context context;
final LoginCredentialsFragment.LoginCredentials credentials;
public ServerConfigurationLoader(Context context, Bundle args) {
super(context);
this.context = context;
credentials = (LoginCredentialsFragment.LoginCredentials)args.getSerializable(ARG_LOGIN_CREDENTIALS);
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
public ServerConfiguration loadInBackground() {
DavResourceFinder finder = new DavResourceFinder(context, credentials);
ServerConfiguration configuration = finder.findInitialConfiguration();
try {
@Cleanup BufferedReader logStream = new BufferedReader(new StringReader(configuration.getLogs()));
Constants.log.info("Successful resource detection:");
String line;
while ((line = logStream.readLine()) != null)
Constants.log.debug(line);
} catch (IOException e) {
Constants.log.error("Couldn't read resource detection logs", e);
}
return configuration;
}
}
}

View File

@ -6,20 +6,26 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.ui;
package at.bitfire.davdroid.ui.setup;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import at.bitfire.davdroid.R;
public class AddAccountActivity extends AppCompatActivity {
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login_activity);
if (savedInstanceState == null)
// first call, add fragment
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment, new LoginCredentialsFragment())
.commit();
setContentView(R.layout.activity_add_account);
}
}

View File

@ -0,0 +1,179 @@
/*
* Copyright © 2013 2016 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.ui.setup;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v7.widget.AppCompatCheckBox;
import android.support.v7.widget.AppCompatRadioButton;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import java.io.Serializable;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.ui.EditPassword;
import lombok.Data;
public class LoginCredentialsFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
AppCompatRadioButton radioUseEmail;
LinearLayout emailDetails;
EditText editEmailAddress;
EditPassword editEmailPassword;
AppCompatRadioButton radioUseURL;
LinearLayout urlDetails;
EditText editBaseURL, editUserName;
EditPassword editUrlPassword;
AppCompatCheckBox checkPreemptiveAuth;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.login_credentials_fragment, container, false);
radioUseEmail = (AppCompatRadioButton)v.findViewById(R.id.login_type_email);
emailDetails = (LinearLayout)v.findViewById(R.id.login_type_email_details);
editEmailAddress = (EditText)v.findViewById(R.id.email_address);
editEmailPassword = (EditPassword)v.findViewById(R.id.email_password);
radioUseURL = (AppCompatRadioButton)v.findViewById(R.id.login_type_url);
urlDetails = (LinearLayout)v.findViewById(R.id.login_type_url_details);
editBaseURL = (EditText)v.findViewById(R.id.base_url);
editUserName = (EditText)v.findViewById(R.id.user_name);
editUrlPassword = (EditPassword)v.findViewById(R.id.url_password);
checkPreemptiveAuth = (AppCompatCheckBox)v.findViewById(R.id.preemptive_auth);
radioUseEmail.setOnCheckedChangeListener(this);
radioUseURL.setOnCheckedChangeListener(this);
if (savedInstanceState == null)
radioUseEmail.setChecked(true);
final Button login = (Button)v.findViewById(R.id.login);
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LoginCredentials credentials = validateLoginData();
if (credentials != null) {
// login data OK, continue with DetectConfigurationFragment
Bundle args = new Bundle(1);
args.putSerializable(DetectConfigurationFragment.ARG_LOGIN_CREDENTIALS, credentials);
DialogFragment dialog = new DetectConfigurationFragment();
dialog.setArguments(args);
dialog.show(getFragmentManager(), DetectConfigurationFragment.class.getName());
}
}
});
return v;
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
boolean loginByEmail = buttonView == radioUseEmail;
emailDetails.setVisibility(loginByEmail ? View.VISIBLE : View.GONE);
urlDetails.setVisibility(loginByEmail ? View.GONE : View.VISIBLE);
}
}
protected LoginCredentials validateLoginData() {
if (radioUseEmail.isChecked()) {
URI uri = null;
boolean valid = true;
String email = editEmailAddress.getText().toString();
if (!email.matches(".+@.+")) {
editEmailAddress.setError(getString(R.string.login_email_address_error));
valid = false;
} else
try {
uri = new URI("mailto", email, null);
} catch (URISyntaxException e) {
editEmailAddress.setError(e.getLocalizedMessage());
valid = false;
}
String password = editEmailPassword.getText().toString();
if (password.isEmpty()) {
editEmailPassword.setError(getString(R.string.login_password_required));
valid = false;
}
return valid ? new LoginCredentials(uri, email, password, true) : null;
} else if (radioUseURL.isChecked()) {
URI uri = null;
boolean valid = true;
String host = null, path = null;
int port = -1;
Uri baseUrl = Uri.parse(editBaseURL.getText().toString());
String scheme = baseUrl.getScheme();
if ("https".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) {
host = IDN.toASCII(baseUrl.getHost());
if (host.isEmpty()) {
editBaseURL.setError(getString(R.string.login_url_host_name_required));
valid = false;
}
path = baseUrl.getEncodedPath();
port = baseUrl.getPort();
try {
uri = new URI(baseUrl.getScheme(), null, host, port, path, null, null);
} catch (URISyntaxException e) {
editBaseURL.setError(e.getLocalizedMessage());
valid = false;
}
} else {
editBaseURL.setError(getString(R.string.login_url_must_be_http_or_https));
valid = false;
}
String userName = editUserName.getText().toString();
if (userName.isEmpty()) {
editUserName.setError(getString(R.string.login_user_name_required));
valid = false;
}
String password = editUrlPassword.getText().toString();
if (password.isEmpty()) {
editUrlPassword.setError(getString(R.string.login_password_required));
valid = false;
}
return valid ? new LoginCredentials(uri, userName, password, checkPreemptiveAuth.isChecked()) : null;
}
return null;
}
@Data
public class LoginCredentials implements Serializable {
final URI uri;
final String userName, password;
final boolean authPreemptive;
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2013 2015 Ricki Hirner (bitfire web engineering).
~ Copyright © 2013 2016 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
@ -8,8 +8,8 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/right_pane"
android:layout_width="match_parent"
android:layout_height="match_parent" >
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/fragment">
</FrameLayout>

View File

@ -12,39 +12,52 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- We don't want the keyboard up when the user arrives in this initial screen -->
<View android:layout_height="0dp"
android:layout_width="0dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:contentDescription="@null"
android:importantForAccessibility="no">
<requestFocus/>
</View>
<ScrollView android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin">
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:animateLayoutChanges="true">
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/login_type_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_email"
android:checked="true" />
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_email_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_email_address"/>
<EditText
android:id="@+id/email_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_password"/>
android:hint="@string/login_email_address"
android:inputType="textEmailAddress"/>
<at.bitfire.davdroid.ui.EditPassword
android:id="@+id/email_password"
android:hint="@string/login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
@ -53,42 +66,40 @@
android:id="@+id/login_type_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url" />
android:text="@string/login_type_url"
android:layout_marginTop="16dp"
style="@style/login_type_headline"/>
<LinearLayout
android:id="@+id/login_type_url_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_base_url"/>
<EditText
android:id="@+id/base_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_user_name"/>
android:hint="@string/login_base_url"
android:inputType="textUri"/>
<EditText
android:id="@+id/user_name"
android:layout_width="match_parent"
android:maxWidth="100dp"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_password"/>
android:hint="@string/login_user_name"
android:inputType="textNoSuggestions"/>
<at.bitfire.davdroid.ui.EditPassword
android:id="@+id/url_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
android:hint="@string/login_password"/>
<android.support.v7.widget.AppCompatCheckBox
android:id="@+id/preemptive_auth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/login_auth_preemptive"/>
android:text="@string/login_auth_preemptive"
android:checked="true"/>
</LinearLayout>
@ -100,26 +111,17 @@
android:layout_height="wrap_content"
style="@style/stepper_nav_bar">
<Button
<Space
android:layout_width="0dp"
android:layout_weight="1"
android:text="Back"
android:visibility="invisible"
style="@style/stepper_nav_button"/>
<Button
android:id="@+id/login"
android:layout_width="0dp"
android:layout_weight="1"
android:text="Login"
style="@style/stepper_nav_button"/>
<Button
android:layout_width="0dp"
android:layout_weight="1"
android:text="Create account"
android:visibility="gone"
style="@style/stepper_nav_button"/>
</LinearLayout>
</LinearLayout>

View File

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scrollbars="vertical" >
<GridLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="2"
android:padding="10dp"
android:useDefaultMargins="true" >
<TextView
android:layout_columnSpan="2"
android:layout_gravity="start"
android:layout_marginBottom="20dp"
android:text="@string/setup_account_details"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:layout_gravity="start"
android:text="@string/setup_account_name"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/account_name"
android:layout_gravity="fill_horizontal"
android:hint="@string/setup_account_name_hint"
android:inputType="textEmailAddress">
<requestFocus />
</EditText>
<TextView
android:id="@+id/account_name_info"
android:layout_width="fill_parent"
android:layout_columnSpan="2"
android:layout_gravity="start"
android:drawableLeft="@drawable/extra_actions_about"
android:drawableStart="@drawable/extra_actions_about"
android:drawablePadding="10dp"
android:padding="10dp"
android:text="@string/setup_account_name_info"
android:textAppearance="?android:attr/textAppearanceMedium" />
<Space
android:layout_gravity="start|top"
android:layout_row="3" />
</GridLayout>
</ScrollView>

View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
style="@style/TextView.Heading"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/setup_address_books" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:text="@string/setup_select_address_book" />
</LinearLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
style="@style/TextView.Heading"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/setup_calendars" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:text="@string/setup_select_calendars" />
</LinearLayout>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="20dp"
tools:context=".MainActivity" >
<LinearLayout android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/setup_install_apps_info"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:id="@+id/setup_install_tasks_app"
android:text="@string/setup_install_tasks_app_html"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
</ScrollView>

View File

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<GridLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="2"
android:padding="10dp"
android:useDefaultMargins="true" >
<TextView
android:layout_columnSpan="2"
android:layout_gravity="start"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/login_email_description" />
<TextView
android:labelFor="@+id/email_address"
android:text="@string/login_email_address"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/email_address"
android:layout_gravity="fill_horizontal"
android:inputType="textNoSuggestions|textEmailAddress"
android:imeOptions="actionNext"
android:layout_width="0dp"
android:scrollHorizontally="true"
android:scrollbars="horizontal"
android:hint="myaccount@myservice.com" tools:ignore="HardcodedText">
<requestFocus />
</EditText>
<TextView
android:labelFor="@+id/password"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/login_password" />
<EditText
android:id="@+id/password"
android:layout_gravity="fill_horizontal"
android:inputType="textPassword"
android:layout_width="0dp"
android:scrollHorizontally="true"
android:scrollbars="horizontal"
android:text="" />
</GridLayout>
</ScrollView>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:useDefaultMargins="true" >
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<RadioButton
android:id="@+id/login_type_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/login_type_email" />
<TextView
android:labelFor="@+id/login_type_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/login_type_email_description" />
<RadioButton
android:id="@+id/login_type_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url" />
<TextView
android:labelFor="@+id/login_type_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_type_url_description" />
</RadioGroup>
</ScrollView>

View File

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<GridLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:columnCount="2"
android:padding="10dp"
android:useDefaultMargins="true" >
<TextView
android:layout_columnSpan="2"
android:layout_gravity="start"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/login_base_url" />
<Spinner
android:id="@+id/login_scheme"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:entries="@array/login_url_scheme" />
<EditText
android:id="@+id/login_host_path"
android:layout_gravity="fill_horizontal"
android:imeOptions="flagForceAscii|actionNext"
android:inputType="textUri"
android:layout_width="0dp"
android:scrollHorizontally="true"
android:scrollbars="horizontal"
android:hint="my.webhost.com" tools:ignore="HardcodedText">
<requestFocus />
</EditText>
<TextView
android:id="@+id/http_warning"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:layout_columnSpan="2"
android:layout_gravity="start"
android:drawableLeft="@drawable/alerts_and_states_warning"
android:drawableStart="@drawable/alerts_and_states_warning"
android:drawablePadding="10dp"
android:padding="10dp"
android:text="@string/login_http_warning" />
<TextView
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/login_user_name" />
<EditText
android:id="@+id/userName"
android:layout_gravity="fill_horizontal"
android:inputType="textNoSuggestions|textEmailAddress"
android:layout_width="0dp"
android:scrollHorizontally="true"
android:scrollbars="horizontal"
android:text="" />
<TextView
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/login_password" />
<EditText
android:id="@+id/password"
android:layout_gravity="fill_horizontal"
android:inputType="textPassword"
android:layout_width="0dp"
android:scrollHorizontally="true"
android:scrollbars="horizontal"
android:text="" />
<CheckBox
android:id="@+id/auth_preemptive"
android:layout_columnSpan="2"
android:checked="true"
android:layout_gravity="start"
android:text="@string/login_auth_preemptive" />
<Space android:layout_gravity="start|top" />
</GridLayout>
</ScrollView>

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="?android:dividerHorizontal"
android:orientation="vertical"
android:paddingBottom="20dp"
android:showDividers="end" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:paddingBottom="10dp"
android:text="@string/setup_what_to_sync"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
style="@style/TextView.Heading"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/setup_task_lists" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:text="@string/setup_select_task_lists" />
</LinearLayout>

View File

@ -53,16 +53,16 @@ Dieses Programm ist freie Software. Sie können es unter den Bedingungen der <a
<string name="login_type_url">Mit URL und Benutzername anmelden</string>
<string name="login_type_url_description">Basis-URL und Benutzername werden verwendet, um die Servereinstellungen herauszufinden; z.B. bei einem eigenen Server.</string>
<string name="login_email_description">Geben Sie Ihre Email-Adresse ein. Der Domänenname wird verwendet, um die Servereinstellungen herauszufinden.</string>
<string name="login_email_address">Email:</string>
<string name="login_email_address">Email-Adresse</string>
<string-array name="login_url_scheme">
<item>http://</item>
<item>https://</item>
</string-array>
<string name="login_http_warning">Ohne Verschlüsselung (HTTPS) können Ihre Zugangsdaten, Kontakte und Termine leicht abgefangen werden.</string>
<string name="login_user_name">Benutzername:</string>
<string name="login_base_url">Basis-URL (Ordner werden automatisch gefunden):</string>
<string name="login_user_name">Benutzername</string>
<string name="login_base_url">Basis-URL:</string>
<string name="login_auth_preemptive">Präemptive Authentifizierung (empfohlen, aber nicht kompatibel mit Digest-Auth.)</string>
<string name="login_password">Passwort:</string>
<string name="login_password">Passwort</string>
<!--Settings activity-->
<string name="settings_title">Einstellungen</string>
<string name="settings_no_accounts">Keine DAVdroid-Konten gefunden</string>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © 2013 2016 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
-->
<resources>
<declare-styleable name="EditPassword">
<attr name="text" format="string"/>
</declare-styleable>
</resources>

View File

@ -11,7 +11,7 @@
<dimen name="leftcol_width">320dp</dimen>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="activity_vertical_margin">20dp</dimen>
<dimen name="fab_margin">16dp</dimen>

View File

@ -93,23 +93,20 @@
<!-- AddAccountActivity -->
<string name="login_title">Add account</string>
<string name="login_type_email">Login with email address</string>
<string name="login_type_email_description">Service details will be auto-detected by domain name. Example: myaccount@icloud.com</string>
<string name="login_email_address">Email address</string>
<string name="login_email_address_error">Valid email address required</string>
<string name="login_password">Password</string>
<string name="login_password_required">Password required</string>
<string name="login_type_url">Login with URL and user name</string>
<string name="login_type_url_description">Service details will be auto-detected by initial URL and user name. Mostly used for self-hosted services.</string>
<string name="login_email_description">Please enter your email address. Its domain name will be used to auto-detect service settings.</string>
<string name="login_email_address">Email:</string>
<string-array name="login_url_scheme">
<item>http://</item>
<item>https://</item>
</string-array>
<string name="login_http_warning">"If you don't use encryption (HTTPS), other people may easily intercept your login details, contacts and events."</string>
<string name="login_user_name">User name:</string>
<string name="login_base_url">Base URL (collections will be auto-detected):</string>
<string name="login_url_must_be_http_or_https">URL must begin with http(s)://</string>
<string name="login_url_host_name_required">Host name required</string>
<string name="login_user_name">User name</string>
<string name="login_user_name_required">User name required</string>
<string name="login_base_url">Base URL</string>
<string name="login_auth_preemptive">Preemptive authentication (recommended, but incompatible with Digest auth)</string>
<string name="login_password">Password:</string>
<string name="login_configuration_detection">Configuration detection</string>
<string name="login_querying_server">Please wait, querying server…</string>
<!-- Settings activity -->
<string name="settings_title">Settings</string>

View File

@ -35,6 +35,14 @@
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
<!-- AddAccountActivity -->
<style name="login_type_headline">
<item name="android:paddingLeft">14dp</item>
<item name="android:textSize">20dp</item>
</style>
<!-- stepper (wizard) -->
<style name="stepper_nav_bar">

@ -1 +1 @@
Subproject commit adb19e52298c11e1abc7d9aeee15380f835f1ba2
Subproject commit c67d40c47e792af812866289b6809dc8ccf49fd8

@ -1 +1 @@
Subproject commit 781cd6254258a243deea542ca71bd1a86c996ea6
Subproject commit 343c3b3ec03b331202425f377b57a17ef2ec3d85