Migrated to Android Studio/gradle

* moved all dependencies to gradle instead of shipping .jar files in the app/lib directory
* switched to official Android port of HttpClient instead of httpclientandroidlib
* new .gitignore and project files
pull/2/head
R Hirner 10 years ago
commit d56175652c

86
.gitignore vendored

@ -0,0 +1,86 @@
# Created by https://www.gitignore.io
### Android ###
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Gradle ###
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting

1
app/.gitignore vendored

@ -0,0 +1 @@
build

@ -0,0 +1,62 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "at.bitfire.davdroid"
minSdkVersion 14
targetSdkVersion 21
testApplicationId "at.bitfire.davdroid.test"
testInstrumentationRunner "android.test.InstrumentationTestRunner"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
dexOptions {
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
}
dependencies {
// Apache Commons
compile 'commons-lang:commons-lang:2.6'
compile 'org.apache.commons:commons-io:1.3.2'
// Lombok for useful @helpers
compile 'org.projectlombok:lombok:1.14.8'
// ical4j for parsing/generating iCalendars
compile 'org.mnode.ical4j:ical4j:1.0.6'
// ez-vcard for parsing/generating VCards
compile('com.googlecode.ez-vcard:ez-vcard:0.9.6') {
// hCard functionality not needed
exclude group: 'org.jsoup', module: 'jsoup'
exclude group: 'org.freemarker', module: 'freemarker'
// jCard functionality not needed
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
}
// dnsjava for querying SRV/TXT records
compile 'dnsjava:dnsjava:2.1.6'
// HttpClient 4.3, Android flavour for WebDAV operations
compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
// SimpleXML for parsing and generating WebDAV messages
compile('org.simpleframework:simple-xml:2.7.1') {
exclude group: 'stax', module: 'stax-api'
exclude group: 'xpp3', module: 'xpp3'
}
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="ExtraTranslation" severity="warning" />
<issue id="MissingTranslation" severity="warning" />
</lint>

@ -0,0 +1,65 @@
/*******************************************************************************
* Copyright (c) 2014 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.resource.test;
import java.io.IOException;
import java.io.InputStream;
import ezvcard.property.Email;
import ezvcard.property.Telephone;
import lombok.Cleanup;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.resource.Contact;
import at.bitfire.davdroid.resource.InvalidResourceException;
public class ContactTest extends InstrumentationTestCase {
AssetManager assetMgr;
public void setUp() throws IOException, InvalidResourceException {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
}
public void testReferenceVCard() throws IOException, InvalidResourceException {
Contact c = parseVCF("reference.vcf");
assertEquals("Gump", c.getFamilyName());
assertEquals("Forrest", c.getGivenName());
assertEquals("Forrest Gump", c.getDisplayName());
assertEquals("Bubba Gump Shrimp Co.", c.getOrganization().getValues().get(0));
assertEquals("Shrimp Man", c.getJobTitle());
Telephone phone1 = c.getPhoneNumbers().get(0);
assertEquals("(111) 555-1212", phone1.getText());
assertEquals("WORK", phone1.getParameters("TYPE").get(0));
assertEquals("VOICE", phone1.getParameters("TYPE").get(1));
Telephone phone2 = c.getPhoneNumbers().get(1);
assertEquals("(404) 555-1212", phone2.getText());
assertEquals("HOME", phone2.getParameters("TYPE").get(0));
assertEquals("VOICE", phone2.getParameters("TYPE").get(1));
Email email = c.getEmails().get(0);
assertEquals("forrestgump@example.com", email.getValue());
assertEquals("PREF", email.getParameters("TYPE").get(0));
assertEquals("INTERNET", email.getParameters("TYPE").get(1));
}
public void testParseInvalidUnknownProperties() throws IOException, InvalidResourceException {
Contact c = parseVCF("invalid-unknown-properties.vcf");
assertEquals("VCard with invalid unknown properties", c.getDisplayName());
assertNull(c.getUnknownProperties());
}
protected Contact parseVCF(String fname) throws IOException, InvalidResourceException {
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
Contact c = new Contact(fname, null);
c.parseEntity(in);
return c;
}
}

@ -0,0 +1,127 @@
/*******************************************************************************
* Copyright (c) 2014 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.resource.test;
import java.io.IOException;
import java.io.InputStream;
import lombok.Cleanup;
import net.fortuna.ical4j.data.ParserException;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import android.text.format.Time;
import at.bitfire.davdroid.resource.Event;
import at.bitfire.davdroid.resource.InvalidResourceException;
public class EventTest extends InstrumentationTestCase {
AssetManager assetMgr;
Event eViennaEvolution,
eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
public void setUp() throws IOException, InvalidResourceException {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
eViennaEvolution = parseCalendar("vienna-evolution.ics");
eOnThatDay = parseCalendar("event-on-that-day.ics");
eAllDay1Day = parseCalendar("all-day-1day.ics");
eAllDay10Days = parseCalendar("all-day-10days.ics");
eAllDay0Sec = parseCalendar("all-day-0sec.ics");
//assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
}
public void testStartEndTimes() throws IOException, ParserException {
// event with start+end date-time
assertEquals(1381330800000L, eViennaEvolution.getDtStartInMillis());
assertEquals("Europe/Vienna", eViennaEvolution.getDtStartTzID());
assertEquals(1381334400000L, eViennaEvolution.getDtEndInMillis());
assertEquals("Europe/Vienna", eViennaEvolution.getDtEndTzID());
}
public void testStartEndTimesAllDay() throws IOException, ParserException {
// event with start date only
assertEquals(868838400000L, eOnThatDay.getDtStartInMillis());
assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtStartTzID());
// DTEND missing in VEVENT, must have been set to DTSTART+1 day
assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis());
assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtEndTzID());
// event with start+end date for all-day event (one day)
assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtStartTzID());
assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtEndTzID());
// event with start+end date for all-day event (ten days)
assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtStartTzID());
assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtEndTzID());
// event with start+end date on some day (invalid 0 sec-event)
assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtStartTzID());
// DTEND invalid in VEVENT, must have been set to DTSTART+1 day
assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis());
assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtEndTzID());
}
public void testTimezoneDefToTzId() {
// test valid definition
final String VTIMEZONE_SAMPLE = // taken from RFC 4791, 5.2.2. CALDAV:calendar-timezone Property
"BEGIN:VCALENDAR\n" +
"PRODID:-//Example Corp.//CalDAV Client//EN\n" +
"VERSION:2.0\n" +
"BEGIN:VTIMEZONE\n" +
"TZID:US-Eastern\n" +
"LAST-MODIFIED:19870101T000000Z\n" +
"BEGIN:STANDARD\n" +
"DTSTART:19671029T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
"TZOFFSETFROM:-0400\n" +
"TZOFFSETTO:-0500\n" +
"TZNAME:Eastern Standard Time (US &amp; Canada)\n" +
"END:STANDARD\n" +
"BEGIN:DAYLIGHT\n" +
"DTSTART:19870405T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" +
"TZOFFSETFROM:-0500\n" +
"TZOFFSETTO:-0400\n" +
"TZNAME:Eastern Daylight Time (US &amp; Canada)\n" +
"END:DAYLIGHT\n" +
"END:VTIMEZONE\n" +
"END:VCALENDAR";
assertEquals("US-Eastern", Event.TimezoneDefToTzId(VTIMEZONE_SAMPLE));
// test null value
try {
Event.TimezoneDefToTzId(null);
fail();
} catch(IllegalArgumentException e) {
assert(true);
}
// test invalid time zone
try {
Event.TimezoneDefToTzId("/* invalid content */");
fail();
} catch(IllegalArgumentException e) {
assert(true);
}
}
protected Event parseCalendar(String fname) throws IOException, InvalidResourceException {
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
Event e = new Event(fname, null);
e.parseEntity(in);
return e;
}
}

@ -0,0 +1,154 @@
/*******************************************************************************
* Copyright (c) 2014 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.resource.test;
import java.util.Calendar;
import lombok.Cleanup;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.provider.CalendarContract.Reminders;
import android.test.InstrumentationTestCase;
import android.util.Log;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalStorageException;
public class LocalCalendarTest extends InstrumentationTestCase {
private static final String
TAG = "davroid.LocalCalendarTest",
calendarName = "DAVdroid_Test";
ContentProviderClient providerClient;
Account testAccount = new Account(calendarName, CalendarContract.ACCOUNT_TYPE_LOCAL);
LocalCalendar testCalendar;
// helpers
private Uri syncAdapterURI(Uri uri) {
return uri.buildUpon()
.appendQueryParameter(Calendars.ACCOUNT_NAME, calendarName)
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
build();
}
private long insertNewEvent() throws LocalStorageException, RemoteException {
ContentValues values = new ContentValues();
values.put(Events.CALENDAR_ID, testCalendar.getId());
values.put(Events.TITLE, "Test Event");
values.put(Events.ALL_DAY, 0);
values.put(Events.DTSTART, Calendar.getInstance().getTimeInMillis());
values.put(Events.DTEND, Calendar.getInstance().getTimeInMillis());
values.put(Events.EVENT_TIMEZONE, "UTC");
values.put(Events.DIRTY, 1);
return ContentUris.parseId(providerClient.insert(syncAdapterURI(Events.CONTENT_URI), values));
}
private void deleteEvent(long id) throws RemoteException {
providerClient.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)), null, null);
}
// initialization
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
protected void setUp() throws Exception {
ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
providerClient = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
long id;
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI,
new String[] { Calendars._ID },
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.NAME + "=?",
new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, calendarName },
null);
if (cursor.moveToNext()) {
// found local test calendar
id = cursor.getLong(0);
Log.d(TAG, "Found test calendar with ID " + id);
} else {
// no local test calendar found, create
ContentValues values = new ContentValues();
values.put(Calendars.ACCOUNT_NAME, testAccount.name);
values.put(Calendars.ACCOUNT_TYPE, testAccount.type);
values.put(Calendars.NAME, calendarName);
values.put(Calendars.CALENDAR_DISPLAY_NAME, calendarName);
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
values.put(Calendars.SYNC_EVENTS, 0);
values.put(Calendars.VISIBLE, 1);
if (android.os.Build.VERSION.SDK_INT >= 15) {
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
}
Uri calendarURI = providerClient.insert(syncAdapterURI(Calendars.CONTENT_URI), values);
id = ContentUris.parseId(calendarURI);
Log.d(TAG, "Created test calendar with ID " + id);
}
testCalendar = new LocalCalendar(testAccount, providerClient, id, null);
}
protected void tearDown() throws Exception {
Uri uri = ContentUris.withAppendedId(syncAdapterURI(Calendars.CONTENT_URI), testCalendar.getId());
providerClient.delete(uri, null, null);
}
// tests
public void testCTags() throws LocalStorageException {
assertNull(testCalendar.getCTag());
final String cTag = "just-modified";
testCalendar.setCTag(cTag);
assertEquals(cTag, testCalendar.getCTag());
}
public void testFindNew() throws LocalStorageException, RemoteException {
// at the beginning, there are no dirty events
assertTrue(testCalendar.findNew().length == 0);
assertTrue(testCalendar.findUpdated().length == 0);
// insert a "new" event
long id = insertNewEvent();
try {
// there must be one "new" event now
assertTrue(testCalendar.findNew().length == 1);
assertTrue(testCalendar.findUpdated().length == 0);
// nothing has changed, the record must still be "new"
// see issue #233
assertTrue(testCalendar.findNew().length == 1);
assertTrue(testCalendar.findUpdated().length == 0);
} finally {
deleteEvent(id);
}
}
}

@ -0,0 +1,69 @@
package at.bitfire.davdroid.syncadapter;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.resource.DavResourceFinder;
import at.bitfire.davdroid.resource.ServerInfo;
import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo;
import at.bitfire.davdroid.test.Constants;
import ezvcard.VCardVersion;
public class DavResourceFinderTest extends InstrumentationTestCase {
DavResourceFinder finder;
@Override
protected void setUp() {
finder = new DavResourceFinder(getInstrumentation().getContext());
}
@Override
protected void tearDown() throws IOException {
finder.close();
}
public void testFindResourcesRobohydra() throws Exception {
ServerInfo info = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true);
finder.findResources(info);
// CardDAV
assertTrue(info.isCardDAV());
List<ResourceInfo> collections = info.getAddressBooks();
assertEquals(1, collections.size());
assertEquals("Default Address Book", collections.get(0).getDescription());
assertEquals(VCardVersion.V4_0, collections.get(0).getVCardVersion());
// CalDAV
assertTrue(info.isCalDAV());
collections = info.getCalendars();
assertEquals(2, collections.size());
ResourceInfo resource = collections.get(0);
assertEquals("Private Calendar", resource.getTitle());
assertEquals("This is my private calendar.", resource.getDescription());
assertFalse(resource.isReadOnly());
resource = collections.get(1);
assertEquals("Work Calendar", resource.getTitle());
assertTrue(resource.isReadOnly());
}
public void testGetInitialContextURL() throws Exception {
// without SRV records, but with well-known paths
ServerInfo roboHydra = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true);
assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "caldav"));
assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "carddav"));
// with SRV records and well-known paths
ServerInfo iCloud = new ServerInfo(new URI("mailto:test@icloud.com"), "", "", true);
assertEquals(new URI("https://contacts.icloud.com/"), finder.getInitialContextURL(iCloud, "carddav"));
assertEquals(new URI("https://caldav.icloud.com/"), finder.getInitialContextURL(iCloud, "caldav"));
}
}

@ -0,0 +1,39 @@
/*******************************************************************************
* Copyright (c) 2014 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.test;
import java.util.Arrays;
import junit.framework.TestCase;
import at.bitfire.davdroid.ArrayUtils;
public class ArrayUtilsTest extends TestCase {
public void testPartition() {
// n == 0
assertTrue(Arrays.deepEquals(
new Long[0][0],
ArrayUtils.partition(new Long[] { }, 5)));
// n < max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l } },
ArrayUtils.partition(new Long[] { 1l, 2l }, 5)));
// n == max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l }, { 3l, 4l } },
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2)));
// n > max
assertTrue(Arrays.deepEquals(
new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } },
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5)));
}
}

@ -0,0 +1,19 @@
package at.bitfire.davdroid.test;
import java.net.URI;
import java.net.URISyntaxException;
import android.util.Log;
public class Constants {
public static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/";
public static URI roboHydra;
static {
try {
roboHydra = new URI(ROBOHYDRA_BASE);
} catch(URISyntaxException e) {
Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL");
}
}
}

@ -0,0 +1,62 @@
/*******************************************************************************
* Copyright (c) 2014 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.test;
import java.io.IOException;
import java.io.InputStream;
import lombok.Cleanup;
import net.fortuna.ical4j.data.ParserException;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.resource.Contact;
import ezvcard.property.Impp;
public class ContactTest extends InstrumentationTestCase {
AssetManager assetMgr;
public void setUp() {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
}
public void testIMPP() throws IOException {
Contact c = parseVCard("impp.vcf");
assertEquals("test mctest", c.getDisplayName());
Impp jabber = c.getImpps().get(0);
assertNull(jabber.getProtocol());
assertEquals("test-without-valid-scheme@test.tld", jabber.getHandle());
}
public void testParseVcard3() throws IOException, ParserException {
Contact c = parseVCard("vcard3-sample1.vcf");
assertEquals("Forrest Gump", c.getDisplayName());
assertEquals("Forrest", c.getGivenName());
assertEquals("Gump", c.getFamilyName());
assertEquals(2, c.getPhoneNumbers().size());
assertEquals("(111) 555-1212", c.getPhoneNumbers().get(0).getText());
assertEquals(1, c.getEmails().size());
assertEquals("forrestgump@example.com", c.getEmails().get(0).getValue());
assertFalse(c.isStarred());
}
private Contact parseVCard(String fileName) throws IOException {
@Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING);
Contact c = new Contact(fileName, null);
c.parseEntity(in);
return c;
}
}

@ -0,0 +1,61 @@
/*******************************************************************************
* Copyright (c) 2014 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.test;
import java.net.URI;
import junit.framework.TestCase;
import at.bitfire.davdroid.URIUtils;
public class URLUtilsTest extends TestCase {
/* RFC 1738 p17 HTTP URLs:
hpath = hsegment *[ "/" hsegment ]
hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ]
uchar = unreserved | escape
unreserved = alpha | digit | safe | extra
alpha = lowalpha | hialpha
lowalpha = ...
hialpha = ...
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
"8" | "9"
safe = "$" | "-" | "_" | "." | "+"
extra = "!" | "*" | "'" | "(" | ")" | ","
escape = "%" hex hex
*/
public void testEnsureTrailingSlash() throws Exception {
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test"));
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test/"));
String withoutSlash = "http://www.test.at/dav/collection",
withSlash = withoutSlash + "/";
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withoutSlash)));
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withSlash)));
}
public void testParseURI() throws Exception {
// don't escape valid characters
String validPath = "/;:@&=$-_.+!*'(),";
assertEquals(new URI("https://www.test.at:123" + validPath), URIUtils.parseURI("https://www.test.at:123" + validPath));
assertEquals(new URI(validPath), URIUtils.parseURI(validPath));
// keep literal IPv6 addresses (only in host name)
assertEquals(new URI("https://[1:2::1]/"), URIUtils.parseURI("https://[1:2::1]/"));
// ~ as home directory
assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/~user1/"));
assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/%7euser1/"));
// @ in directory name
assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user@server.com/"));
assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user%40server.com/"));
}
}

@ -0,0 +1,73 @@
package at.bitfire.davdroid.webdav;
import java.io.IOException;
import junit.framework.TestCase;
import at.bitfire.davdroid.test.Constants;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.methods.HttpOptions;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
public class DavRedirectStrategyTest extends TestCase {
CloseableHttpClient httpClient;
DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE;
@Override
protected void setUp() {
httpClient = HttpClientBuilder.create()
.useSystemProperties()
.disableRedirectHandling()
.build();
}
@Override
protected void tearDown() throws IOException {
httpClient.close();
}
// happy cases
public void testNonRedirection() throws Exception {
HttpUriRequest request = new HttpOptions(Constants.roboHydra);
HttpResponse response = httpClient.execute(request);
assertFalse(strategy.isRedirected(request, response, null));
}
public void testDefaultRedirection() throws Exception {
final String newLocation = "/new-location";
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/301?to=" + newLocation));
HttpResponse response = httpClient.execute(request, context);
assertTrue(strategy.isRedirected(request, response, context));
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
assertEquals(Constants.roboHydra.resolve(newLocation), redirected.getURI());
}
// error cases
public void testMissingLocation() throws Exception {
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/without-location"));
HttpResponse response = httpClient.execute(request, context);
assertFalse(strategy.isRedirected(request, response, context));
}
public void testRelativeLocation() throws Exception {
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/relative"));
HttpResponse response = httpClient.execute(request, context);
assertTrue(strategy.isRedirected(request, response, context));
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
assertEquals(Constants.roboHydra.resolve("/new/location"), redirected.getURI());
}
}

@ -0,0 +1,27 @@
package at.bitfire.davdroid.webdav;
import java.io.IOException;
import javax.net.ssl.SSLSocket;
import android.util.Log;
import junit.framework.TestCase;
public class TlsSniSocketFactoryTest extends TestCase {
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
public void testCiphers() throws IOException {
SSLSocket socket = (SSLSocket)TlsSniSocketFactory.INSTANCE.createSocket(null);
Log.i(TAG, "Enabled:");
for (String cipher : socket.getEnabledCipherSuites())
Log.i(TAG, cipher);
Log.i(TAG, "Supported:");
for (String cipher : socket.getSupportedCipherSuites())
Log.i(TAG, cipher);
assert(true);
}
}

@ -0,0 +1,233 @@
/*******************************************************************************
* Copyright (c) 2014 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.webdav;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLPeerUnverifiedException;
import lombok.Cleanup;
import org.apache.commons.io.IOUtils;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.test.Constants;
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
// tests require running robohydra!
public class WebDavResourceTest extends InstrumentationTestCase {
static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
final static String PATH_SIMPLE_FILE = "collection/new.file";
AssetManager assetMgr;
CloseableHttpClient httpClient;
WebDavResource baseDAV;
WebDavResource simpleFile,
davCollection, davNonExistingFile, davExistingFile,
davInvalid;
@Override
protected void setUp() throws Exception {
httpClient = DavHttpClient.create(true, true);
assetMgr = getInstrumentation().getContext().getResources().getAssets();
baseDAV = new WebDavResource(httpClient, Constants.roboHydra.resolve("/dav/"));
simpleFile = new WebDavResource(httpClient, Constants.roboHydra.resolve("assets/test.random"));
davCollection = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav/"));
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
davInvalid = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav-invalid/"));
}
@Override
protected void tearDown() throws Exception {
httpClient.close();
}
/* test feature detection */
public void testOptions() throws Exception {
String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE", "REPORT" },
davCapabilities = new String[] { "addressbook", "calendar-access" };
WebDavResource capable = new WebDavResource(baseDAV);
capable.options();
for (String davMethod : davMethods)
assertTrue(capable.supportsMethod(davMethod));
for (String capability : davCapabilities)
assertTrue(capable.supportsDAV(capability));
}
public void testPropfindCurrentUserPrincipal() throws Exception {
davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
assertEquals("/dav/principals/users/test", davCollection.getCurrentUserPrincipal());
try {
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
fail();
} catch(DavException ex) {
}
assertNull(simpleFile.getCurrentUserPrincipal());
}
public void testPropfindHomeSets() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "principals/users/test");
dav.propfind(HttpPropfind.Mode.HOME_SETS);
assertEquals("/dav/addressbooks/test/", dav.getAddressbookHomeSet());
assertEquals("/dav/calendars/test/", dav.getCalendarHomeSet());
}
public void testPropfindAddressBooks() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test");
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
assertEquals(2, dav.getMembers().size());
for (WebDavResource member : dav.getMembers()) {
if (member.getName().equals("default-v4.vcf"))
assertTrue(member.isAddressBook());
else
assertFalse(member.isAddressBook());
assertFalse(member.isCalendar());
}
}
public void testPropfindCalendars() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
dav.propfind(Mode.CALDAV_COLLECTIONS);
assertEquals(3, dav.getMembers().size());
assertEquals("0xFF00FF", dav.getMembers().get(2).getColor());
for (WebDavResource member : dav.getMembers()) {
if (member.getName().contains(".ics"))
assertTrue(member.isCalendar());
else
assertFalse(member.isCalendar());
assertFalse(member.isAddressBook());
}
}
public void testPropfindTrailingSlashes() throws Exception {
final String principalOK = "/principals/ok";
String requestPaths[] = {
"/dav/collection-response-with-trailing-slash",
"/dav/collection-response-with-trailing-slash/",
"/dav/collection-response-without-trailing-slash",
"/dav/collection-response-without-trailing-slash/"
};
for (String path : requestPaths) {
WebDavResource davSlash = new WebDavResource(davCollection, path);
davSlash.propfind(Mode.CARDDAV_COLLECTIONS);
assertEquals(principalOK, davSlash.getCurrentUserPrincipal());
}
}
/* test normal HTTP/WebDAV */
public void testPropfindRedirection() throws Exception {
// PROPFIND redirection
WebDavResource redirected = new WebDavResource(baseDAV, "/redirect/301?to=/dav/");
redirected.propfind(Mode.CURRENT_USER_PRINCIPAL);
assertEquals("/dav/", redirected.getLocation().getPath());
}
public void testGet() throws Exception {
simpleFile.get("*/*");
@Cleanup InputStream is = assetMgr.open("test.random", AssetManager.ACCESS_STREAMING);
byte[] expected = IOUtils.toByteArray(is);
assertTrue(Arrays.equals(expected, simpleFile.getContent()));
}
public void testGetHttpsWithSni() throws Exception {
WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch"));
boolean sniWorking = false;
try {
file.get("*/*");
sniWorking = true;
} catch (SSLPeerUnverifiedException e) {
}
assertTrue(sniWorking);
}
public void testMultiGet() throws Exception {
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf");
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" });
assertEquals("My Book", davAddressBook.getDisplayName());
assertEquals(2, davAddressBook.getMembers().size());
for (WebDavResource member : davAddressBook.getMembers())
assertNotNull(member.getContent());
}
public void testPutAddDontOverwrite() throws Exception {
// should succeed on a non-existing file
assertEquals("has-just-been-created", davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE));
// should fail on an existing file
try {
davExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
fail();
} catch(PreconditionFailedException ex) {
}
}
public void testPutUpdateDontOverwrite() throws Exception {
// should succeed on an existing file
assertEquals("has-just-been-updated", davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE));
// should fail on a non-existing file
try {
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
fail();
} catch(PreconditionFailedException ex) {
}
}
public void testDelete() throws Exception {
// should succeed on an existing file
davExistingFile.delete();
// should fail on a non-existing file
try {
davNonExistingFile.delete();
fail();
} catch (NotFoundException e) {
}
}
/* test CalDAV/CardDAV */
/* special test */
public void testInvalidURLs() throws Exception {
WebDavResource dav = new WebDavResource(davInvalid, "addressbooks/%7euser1/");
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
List<WebDavResource> members = dav.getMembers();
assertEquals(1, members.size());
assertEquals(Constants.ROBOHYDRA_BASE + "dav-invalid/addressbooks/~user1/My%20Contacts:1.vcf/", members.get(0).getLocation().toASCIIString());
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DavdroidTest</string>
</resources>

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="47"
android:versionName="0.6.8" android:installLocation="internalOnly">
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:process=":sync" >
<service
android:name=".syncadapter.AccountAuthenticatorService"
android:exported="false" >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/account_authenticator" />
</service>
<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true" >
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_contacts" />
<meta-data
android:name="android.provider.CONTACTS_STRUCTURE"
android:resource="@xml/contacts" />
</service>
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true" >
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_calendars" />
</service>
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".syncadapter.AddAccountActivity"
android:excludeFromRecents="true" >
</activity>
<activity
android:name=".syncadapter.GeneralSettingsActivity" >
</activity>
</application>
</manifest>

@ -0,0 +1,33 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import java.lang.reflect.Array;
public class ArrayUtils {
@SuppressWarnings("unchecked")
public static <T> T[][] partition(T[] bigArray, int max) {
int nItems = bigArray.length;
int nPartArrays = (nItems + max-1)/max;
T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0);
// nItems is now the number of remaining items
for (int i = 0; nItems > 0; i++) {
int n = (nItems < max) ? nItems : max;
partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n);
System.arraycopy(bigArray, i*max, partArrays[i], 0, n);
nItems -= n;
}
return partArrays;
}
}

@ -0,0 +1,18 @@
/*******************************************************************************
* Copyright (c) 2014 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;
public class Constants {
public static final String
APP_VERSION = "0.6.9",
ACCOUNT_TYPE = "bitfire.at.davdroid",
WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app",
SETTING_DISABLE_COMPRESSION = "disable_compression",
SETTING_NETWORK_LOGGING = "network_logging";
}

@ -0,0 +1,82 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import at.bitfire.davdroid.syncadapter.AddAccountActivity;
import at.bitfire.davdroid.syncadapter.GeneralSettingsActivity;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tvWorkaround = (TextView)findViewById(R.id.text_workaround);
if (fromPlayStore()) {
tvWorkaround.setVisibility(View.VISIBLE);
tvWorkaround.setText(Html.fromHtml(getString(R.string.html_main_workaround)));
tvWorkaround.setMovementMethod(LinkMovementMethod.getInstance());
}
TextView tvInfo = (TextView)findViewById(R.id.text_info);
tvInfo.setText(Html.fromHtml(getString(R.string.html_main_info, Constants.APP_VERSION)));
tvInfo.setMovementMethod(LinkMovementMethod.getInstance());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_activity, menu);
return true;
}
public void addAccount(MenuItem item) {
Intent intent = new Intent(this, AddAccountActivity.class);
startActivity(intent);
}
public void showDebugSettings(MenuItem item) {
Intent intent = new Intent(this, GeneralSettingsActivity.class);
startActivity(intent);
}
public void showSyncSettings(MenuItem item) {
Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
startActivity(intent);
}
public void showWebsite(MenuItem item) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(Constants.WEB_URL_HELP + "&pk_kwd=main-activity"));
startActivity(intent);
}
private boolean fromPlayStore() {
try {
return "com.android.vending".equals(getPackageManager().getInstallerPackageName("at.bitfire.davdroid"));
} catch(IllegalArgumentException e) {
}
return false;
}
}

@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2014 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;
import java.net.URI;
import java.net.URISyntaxException;
import android.util.Log;
public class URIUtils {
private static final String TAG = "davdroid.URIUtils";
public static String ensureTrailingSlash(String href) {
if (!href.endsWith("/")) {
Log.d(TAG, "Implicitly appending trailing slash to collection " + href);
return href + "/";
} else
return href;
}
public static URI ensureTrailingSlash(URI href) {
if (!href.getPath().endsWith("/")) {
try {
URI newURI = new URI(href.getScheme(), href.getAuthority(), href.getPath() + "/", null, null);
Log.d(TAG, "Appended trailing slash to collection " + href + " -> " + newURI);
href = newURI;
} catch (URISyntaxException e) {
}
}
return href;
}