various improvements

* fixed annoying bug where all imported contacts where starred
* checkbox for preemptive authentication in setup
* support for (non-preemptive) Digest authentication
* add UID when received entities don't contain one
* version bump to 0.3.2-alpha
* build.xml for ant/Fdroid
pull/2/head
rfc2822 11 years ago
parent 6727987051
commit 9a9d9709c9

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="4"
android:versionName="0.3.1-alpha" >
android:versionCode="5"
android:versionName="0.3.2-alpha" >
<uses-sdk
android:minSdkVersion="14"

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="davdroid" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

@ -62,9 +62,16 @@
android:layout_gravity="fill_horizontal"
android:inputType="textPassword"
android:text="" />
<CheckBox
android:id="@+id/auth_preemptive"
android:layout_columnSpan="2"
android:checked="true"
android:layout_gravity="left"
android:text="@string/auth_preemptive" />
<Space android:layout_gravity="left|top" />
</GridLayout>
</ScrollView>

@ -24,5 +24,5 @@
<string name="calendars">Kalender</string>
<string name="select_address_book">Ein oder kein Adressbuch auswählen (nochmal berühren, um abzuwählen):</string>
<string name="select_calendars">Kalender zur Synchronisation auswählen:</string>
<string name="auth_preemptive">Präemptive Authentifizierung (empfohlen, aber nicht kompatibel mit Digest-Auth.)</string>
</resources>

@ -30,5 +30,6 @@
<string name="calendars">Calendars</string>
<string name="select_address_book">Select up to one address book (tap again to unselect):</string>
<string name="select_calendars">Select your calendars:</string>
<string name="auth_preemptive">Preemptive authentification (recommended, but incompatible with Digest auth)</string>
</resources>

@ -9,12 +9,14 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.3-alpha",
APP_VERSION = "0.3.2-alpha",
ACCOUNT_TYPE = "bitfire.at.davdroid",
ACCOUNT_KEY_USERNAME = "user_name",
ACCOUNT_KEY_BASE_URL = "principal_url",
ACCOUNT_KEY_AUTH_PREEMPTIVE = "auth_preemptive",
ACCOUNT_KEY_ADDRESSBOOK_PATH = "addressbook_path",
ACCOUNT_KEY_ADDRESSBOOK_CTAG = "addressbook_ctag",

@ -31,7 +31,7 @@ public class CalDavCalendar extends RemoteCollection<Event> {
}
public CalDavCalendar(String baseURL, String user, String password) throws IOException, URISyntaxException {
super(baseURL, user, password);
public CalDavCalendar(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
super(baseURL, user, password, preemptiveAuth);
}
}

@ -31,7 +31,7 @@ public class CardDavAddressBook extends RemoteCollection<Contact> {
}
public CardDavAddressBook(String baseURL, String user, String password) throws IOException, URISyntaxException {
super(baseURL, user, password);
public CardDavAddressBook(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
super(baseURL, user, password, preemptiveAuth);
}
}

@ -18,6 +18,7 @@ import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@ -117,6 +118,10 @@ public class Contact extends Resource {
Uid uid = (Uid)vcard.getProperty(Id.UID);
if (uid != null)
this.uid = uid.getValue();
else {
Log.w(TAG, "Received VCONTACT without UID, generating new one");
this.uid = UUID.randomUUID().toString();
}
Starred starred = (Starred)vcard.getExtendedProperty(Starred.PROPERTY_NAME);
this.starred = starred != null && starred.getValue().equals("1");

@ -48,6 +48,7 @@ import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;
import net.fortuna.ical4j.util.UidGenerator;
import android.text.format.Time;
import android.util.Log;
import at.bitfire.davdroid.Constants;
@ -106,6 +107,11 @@ public class Event extends Resource {
if (event.getUid() != null)
uid = event.getUid().toString();
else {
Log.w(TAG, "Received VEVENT without UID, generating new one");
UidGenerator uidGenerator = new UidGenerator(Integer.toString(android.os.Process.myPid()));
uid = uidGenerator.generateUid().getValue();
}
dtStart = event.getStartDate(); validateTimeZone(dtStart);
dtEnd = event.getEndDate(); validateTimeZone(dtEnd);

@ -12,8 +12,6 @@ import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import lombok.Getter;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList;
@ -28,6 +26,9 @@ import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Status;
import org.apache.commons.lang.StringUtils;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
@ -73,6 +74,12 @@ public class LocalCalendar extends LocalCollection<Event> {
protected String entryColumnDirty() { return Events.DIRTY; }
protected String entryColumnDeleted() { return Events.DELETED; }
@SuppressLint("InlinedApi")
protected String entryColumnUID() {
return (android.os.Build.VERSION.SDK_INT >= 17) ?
Events.UID_2445 : Events.SYNC_DATA2;
}
/* class methods, constructor */
@ -158,9 +165,12 @@ public class LocalCalendar extends LocalCollection<Event> {
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
/* 17 */ entryColumnUID()
}, null, null, null);
if (cursor.moveToNext()) {
e.setUid(cursor.getString(17));
e.setSummary(cursor.getString(0));
e.setLocation(cursor.getString(1));
e.setDescription(cursor.getString(2));
@ -357,6 +367,7 @@ public class LocalCalendar extends LocalCollection<Event> {
.withValue(Events.CALENDAR_ID, id)
.withValue(entryColumnRemoteName(), event.getName())
.withValue(entryColumnETag(), event.getETag())
.withValue(entryColumnUID(), event.getUid())
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
.withValue(Events.DTSTART, event.getDtStartInMillis())
.withValue(Events.DTEND, event.getDtEndInMillis())

@ -9,13 +9,9 @@ package at.bitfire.davdroid.resource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import net.fortuna.ical4j.model.ValidationException;
import org.apache.commons.lang.StringUtils;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
@ -23,7 +19,6 @@ import android.content.ContentProviderOperation.Builder;
import android.content.ContentUris;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.CalendarContract;
@ -50,6 +45,8 @@ public abstract class LocalCollection<ResourceType extends Resource> {
abstract protected String entryColumnDirty();
abstract protected String entryColumnDeleted();
abstract protected String entryColumnUID();
LocalCollection(Account account, ContentProviderClient providerClient) {
this.account = account;

@ -32,8 +32,8 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
abstract protected MultigetType multiGetType();
abstract protected ResourceType newResourceSkeleton(String name, String ETag);
public RemoteCollection(String baseURL, String user, String password) throws IOException, URISyntaxException {
collection = new WebDavCollection(new URI(baseURL), user, password);
public RemoteCollection(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
collection = new WebDavCollection(new URI(baseURL), user, password, preemptiveAuth);
}
@ -60,30 +60,34 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
@SuppressWarnings("unchecked")
public Resource[] multiGet(ResourceType[] resources) throws IOException, IncapableResourceException, HttpException, ParserException {
if (resources.length == 1) {
Resource resource = get(resources[0]);
return (resource != null) ? (ResourceType[]) new Resource[] { resource } : null;
}
LinkedList<String> names = new LinkedList<String>();
for (ResourceType resource : resources)
names.add(resource.getName());
collection.multiGet(names.toArray(new String[0]), multiGetType());
LinkedList<ResourceType> foundResources = new LinkedList<ResourceType>();
for (WebDavResource member : collection.getMembers()) {
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
resource.parseEntity(member.getContent());
foundResources.add(resource);
try {
if (resources.length == 1) {
Resource resource = get(resources[0]);
return (resource != null) ? (ResourceType[]) new Resource[] { resource } : null;
}
LinkedList<String> names = new LinkedList<String>();
for (ResourceType resource : resources)
names.add(resource.getName());
collection.multiGet(names.toArray(new String[0]), multiGetType());
LinkedList<ResourceType> foundResources = new LinkedList<ResourceType>();
for (WebDavResource member : collection.getMembers()) {
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
resource.parseEntity(member.getContent());
foundResources.add(resource);
}
return foundResources.toArray(new Resource[0]);
} catch(ValidationException ex) {
return null;
}
return foundResources.toArray(new Resource[0]);
}
/* internal member operations */
public ResourceType get(ResourceType resource) throws IOException, HttpException, ParserException {
public ResourceType get(ResourceType resource) throws IOException, HttpException, ParserException, ValidationException {
WebDavResource member = new WebDavResource(collection, resource.getName());
member.get();
resource.parseEntity(member.getContent());

@ -35,6 +35,7 @@ import at.bitfire.davdroid.resource.CalDavCalendar;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.WebDavResource;
public class CalendarsSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
@ -75,7 +76,8 @@ public class CalendarsSyncAdapterService extends Service {
URI uri = new URI(accountManager.getUserData(account, Constants.ACCOUNT_KEY_BASE_URL)).resolve(calendar.getPath());
RemoteCollection dav = new CalDavCalendar(uri.toString(),
accountManager.getUserData(account, Constants.ACCOUNT_KEY_USERNAME),
accountManager.getPassword(account));
accountManager.getPassword(account),
Boolean.parseBoolean(accountManager.getUserData(account, Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE)));
syncManager.synchronize(calendar, dav, extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
}

@ -32,12 +32,10 @@ import android.os.RemoteException;
import android.util.Log;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.CardDavAddressBook;
import at.bitfire.davdroid.resource.Contact;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.resource.Resource;
public class ContactsSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
@ -75,8 +73,9 @@ public class ContactsSyncAdapterService extends Service {
RemoteCollection dav = new CardDavAddressBook(
uri.toString(),
accountManager.getUserData(account, Constants.ACCOUNT_KEY_USERNAME),
accountManager.getPassword(account));
accountManager.getUserData(account, Constants.ACCOUNT_KEY_USERNAME),
accountManager.getPassword(account),
Boolean.parseBoolean(accountManager.getUserData(account, Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE)));
LocalCollection database = new LocalAddressBook(account, provider, accountManager);

@ -22,6 +22,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
@ -32,6 +33,7 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
TextView textHttpWarning;
EditText editBaseURL, editUserName, editPassword;
CheckBox checkboxPreemptive;
Button btnNext;
@ -72,6 +74,8 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
editPassword = (EditText) v.findViewById(R.id.password);
editPassword.addTextChangedListener(this);
checkboxPreemptive = (CheckBox) v.findViewById(R.id.auth_preemptive);
// hook into action bar
setHasOptionsMenu(true);
@ -105,6 +109,7 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
args.putString(QueryServerDialogFragment.EXTRA_BASE_URL, protocol + host_path);
args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, editUserName.getText().toString());
args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString());
args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, checkboxPreemptive.isChecked());
DialogFragment dialog = new QueryServerDialogFragment();
dialog.setArguments(args);

@ -35,9 +35,11 @@ import at.bitfire.davdroid.webdav.WebDavResource;
public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks<ServerInfo> {
private static final String TAG = "davdroid.QueryServerDialogFragment";
public static final String EXTRA_BASE_URL = "base_uri",
EXTRA_USER_NAME = "user_name",
EXTRA_PASSWORD = "password";
public static final String
EXTRA_BASE_URL = "base_uri",
EXTRA_USER_NAME = "user_name",
EXTRA_PASSWORD = "password",
EXTRA_AUTH_PREEMPTIVE = "auth_preemptive";
ProgressBar progressBar;
@ -102,12 +104,14 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
ServerInfo serverInfo = new ServerInfo(
args.getString(EXTRA_BASE_URL),
args.getString(EXTRA_USER_NAME),
args.getString(EXTRA_PASSWORD)
args.getString(EXTRA_PASSWORD),
args.getBoolean(EXTRA_AUTH_PREEMPTIVE)
);
try {
// (1/5) detect capabilities
WebDavCollection base = new WebDavCollection(new URI(serverInfo.getBaseURL()), serverInfo.getUserName(), serverInfo.getPassword());
WebDavCollection base = new WebDavCollection(new URI(serverInfo.getBaseURL()), serverInfo.getUserName(),
serverInfo.getPassword(), serverInfo.isAuthPreemptive());
base.options();
serverInfo.setCardDAV(base.supportsDAV("addressbook"));

@ -139,6 +139,7 @@ public class SelectCollectionsFragment extends ListFragment {
Bundle userData = new Bundle();
userData.putString(Constants.ACCOUNT_KEY_BASE_URL, serverInfo.getBaseURL());
userData.putString(Constants.ACCOUNT_KEY_USERNAME, serverInfo.getUserName());
userData.putString(Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive()));
if (!addressBooks.isEmpty()) {
userData.putString(Constants.ACCOUNT_KEY_ADDRESSBOOK_PATH, addressBooks.get(0).getPath());

@ -18,6 +18,7 @@ public class ServerInfo implements Serializable {
final private String baseURL;
final private String userName, password;
final boolean authPreemptive;
private String errorMessage;

@ -8,14 +8,11 @@
package at.bitfire.davdroid.webdav;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.entity.StringEntity;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import android.util.Log;

@ -13,6 +13,7 @@ import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
@ -23,7 +24,6 @@ import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
public class PreemptiveAuthInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);

@ -43,8 +43,8 @@ public class WebDavCollection extends WebDavResource {
@Getter protected List<WebDavResource> members;
public WebDavCollection(URI baseURL, String username, String password) throws IOException {
super(baseURL, username, password);
public WebDavCollection(URI baseURL, String username, String password, boolean preemptiveAuth) throws IOException {
super(baseURL, username, password, preemptiveAuth);
}
public WebDavCollection(WebDavCollection parent, URI member) {

@ -9,8 +9,6 @@ package at.bitfire.davdroid.webdav;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
@ -20,8 +18,6 @@ import java.util.Set;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpException;
@ -68,13 +64,19 @@ public class WebDavResource {
protected DefaultHttpClient client;
public WebDavResource(URI baseURL, String username, String password) throws IOException {
public WebDavResource(URI baseURL, String username, String password, boolean preemptive) throws IOException {
location = baseURL.normalize();
client = new DefaultHttpClient();
client.getCredentialsProvider().setCredentials(new AuthScope(location.getHost(), location.getPort()),
new UsernamePasswordCredentials(username, password));
client.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
// preemptive auth is available for Basic auth only
if (preemptive) {
Log.i(TAG, "Using preemptive Basic Authentication");
client.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
}
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, "DAVdroid");
GzipDecompressingEntity.enable(client);
}

@ -10,6 +10,7 @@ package at.bitfire.davdroid.test;
import java.io.IOException;
import java.io.InputStream;
import net.fortuna.ical4j.data.ParserException;
import junit.framework.Assert;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
@ -23,7 +24,7 @@ public class ContactTest extends InstrumentationTestCase {
}
public void testParseVcard3() throws IOException {
public void testParseVcard3() throws IOException, ParserException {
String fname = "vcard3-sample1.vcf";
InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
@ -35,10 +36,12 @@ public class ContactTest extends InstrumentationTestCase {
Assert.assertEquals("Gump", c.getFamilyName());
Assert.assertEquals(2, c.getPhoneNumbers().size());
Assert.assertEquals("(111) 555-1212", c.getPhoneNumbers().get(0).getText());
Assert.assertEquals("(111) 555-1212", c.getPhoneNumbers().get(0).getValue());
Assert.assertEquals(1, c.getEmails().size());
Assert.assertEquals("forrestgump@example.com", c.getEmails().get(0).getValue());
Assert.assertFalse(c.isStarred());
}
}

Loading…
Cancel
Save