commit d56175652c2f4fc4a3282f343b5c02743555e2df Author: R Hirner Date: Sat Dec 20 20:21:46 2014 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5d711e0f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..f57f6341 --- /dev/null +++ b/app/build.gradle @@ -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' + } +} diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 00000000..d191f74b --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java new file mode 100644 index 00000000..26ee6b1b --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java @@ -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; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java new file mode 100644 index 00000000..89afdc1c --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java @@ -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 & 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 & 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; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java new file mode 100644 index 00000000..6fe5afea --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java @@ -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); + } + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java new file mode 100644 index 00000000..bffd9080 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java @@ -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 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")); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java new file mode 100644 index 00000000..2761d2be --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java @@ -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))); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java b/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java new file mode 100644 index 00000000..e730c9e7 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java @@ -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"); + } + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java new file mode 100644 index 00000000..a98ed78f --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java @@ -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; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java new file mode 100644 index 00000000..7f6a8513 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java @@ -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/")); + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java new file mode 100644 index 00000000..8b92629d --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java @@ -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()); + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java new file mode 100644 index 00000000..f00844d5 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java @@ -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); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java new file mode 100644 index 00000000..9110093d --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java @@ -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 members = dav.getMembers(); + assertEquals(1, members.size()); + assertEquals(Constants.ROBOHYDRA_BASE + "dav-invalid/addressbooks/~user1/My%20Contacts:1.vcf/", members.get(0).getLocation().toASCIIString()); + } + +} diff --git a/app/src/androidTest/res/drawable-hdpi/ic_launcher.png b/app/src/androidTest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..96a442e5 Binary files /dev/null and b/app/src/androidTest/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/androidTest/res/drawable-ldpi/ic_launcher.png b/app/src/androidTest/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 00000000..99238729 Binary files /dev/null and b/app/src/androidTest/res/drawable-ldpi/ic_launcher.png differ diff --git a/app/src/androidTest/res/drawable-mdpi/ic_launcher.png b/app/src/androidTest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..359047df Binary files /dev/null and b/app/src/androidTest/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..71c6d760 Binary files /dev/null and b/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/androidTest/res/values/strings.xml b/app/src/androidTest/res/values/strings.xml new file mode 100644 index 00000000..2539643a --- /dev/null +++ b/app/src/androidTest/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + DavdroidTest + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dfa974cc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java b/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java new file mode 100644 index 00000000..f4f050db --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java @@ -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[][] 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; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/Constants.java b/app/src/main/java/at/bitfire/davdroid/Constants.java new file mode 100644 index 00000000..123ca3a5 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/Constants.java @@ -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"; +} diff --git a/app/src/main/java/at/bitfire/davdroid/MainActivity.java b/app/src/main/java/at/bitfire/davdroid/MainActivity.java new file mode 100644 index 00000000..a665f6a0 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/MainActivity.java @@ -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; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/URIUtils.java b/app/src/main/java/at/bitfire/davdroid/URIUtils.java new file mode 100644 index 00000000..a1ab9038 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/URIUtils.java @@ -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; + } + + + /** + * Parse a received absolute/relative URL and generate a normalized URI that can be compared. + * @param original URI to be parsed, may be absolute or relative + * @return normalized URI + * @throws URISyntaxException + */ + public static URI parseURI(String original) throws URISyntaxException { + URI raw = URI.create(original); + URI uri = new URI(raw.getScheme(), raw.getAuthority(), raw.getPath(), raw.getQuery(), raw.getFragment()); + Log.v(TAG, "Normalized URL " + original + " -> " + uri.toASCIIString()); + return uri; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java new file mode 100644 index 00000000..47a595a9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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; + +import java.net.URISyntaxException; + +import at.bitfire.davdroid.webdav.DavMultiget; +import org.apache.http.impl.client.CloseableHttpClient; + +public class CalDavCalendar extends RemoteCollection { + //private final static String TAG = "davdroid.CalDavCalendar"; + + @Override + protected String memberContentType() { + return "text/calendar"; + } + + @Override + protected DavMultiget.Type multiGetType() { + return DavMultiget.Type.CALENDAR; + } + + @Override + protected Event newResourceSkeleton(String name, String ETag) { + return new Event(name, ETag); + } + + + public CalDavCalendar(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + super(httpClient, baseURL, user, password, preemptiveAuth); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java new file mode 100644 index 00000000..98b9b825 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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; + +import java.net.URISyntaxException; + +import at.bitfire.davdroid.webdav.DavMultiget; +import org.apache.http.impl.client.CloseableHttpClient; + +public class CardDavAddressBook extends RemoteCollection { + //private final static String TAG = "davdroid.CardDavAddressBook"; + + @Override + protected String memberContentType() { + return Contact.MIME_TYPE; + } + + @Override + protected DavMultiget.Type multiGetType() { + return DavMultiget.Type.ADDRESS_BOOK; + } + + @Override + protected Contact newResourceSkeleton(String name, String ETag) { + return new Contact(name, ETag); + } + + + public CardDavAddressBook(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + super(httpClient, baseURL, user, password, preemptiveAuth); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Contact.java b/app/src/main/java/at/bitfire/davdroid/resource/Contact.java new file mode 100644 index 00000000..d301450c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Contact.java @@ -0,0 +1,410 @@ +/******************************************************************************* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.apache.commons.lang.StringUtils; + +import android.util.Log; +import at.bitfire.davdroid.Constants; +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.ValidationWarnings; +import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImageType; +import ezvcard.parameter.TelephoneType; +import ezvcard.property.Address; +import ezvcard.property.Anniversary; +import ezvcard.property.Birthday; +import ezvcard.property.Categories; +import ezvcard.property.Email; +import ezvcard.property.FormattedName; +import ezvcard.property.Impp; +import ezvcard.property.Logo; +import ezvcard.property.Nickname; +import ezvcard.property.Note; +import ezvcard.property.Organization; +import ezvcard.property.Photo; +import ezvcard.property.RawProperty; +import ezvcard.property.Revision; +import ezvcard.property.Role; +import ezvcard.property.Sound; +import ezvcard.property.Source; +import ezvcard.property.StructuredName; +import ezvcard.property.Telephone; +import ezvcard.property.Title; +import ezvcard.property.Uid; +import ezvcard.property.Url; + + +/** + * Represents a contact. Locally, this is a Contact in the Android + * device; remote, this is a VCard. + */ +@ToString(callSuper = true) +public class Contact extends Resource { + private final static String TAG = "davdroid.Contact"; + + public final static String MIME_TYPE = "text/vcard"; + + public final static String + PROPERTY_STARRED = "X-DAVDROID-STARRED", + PROPERTY_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME", + PROPERTY_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME", + PROPERTY_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME", + PROPERTY_SIP = "X-SIP"; + + public final static EmailType EMAIL_TYPE_MOBILE = EmailType.get("X-MOBILE"); + + public final static TelephoneType + PHONE_TYPE_CALLBACK = TelephoneType.get("X-CALLBACK"), + PHONE_TYPE_COMPANY_MAIN = TelephoneType.get("X-COMPANY_MAIN"), + PHONE_TYPE_RADIO = TelephoneType.get("X-RADIO"), + PHONE_TYPE_ASSISTANT = TelephoneType.get("X-ASSISTANT"), + PHONE_TYPE_MMS = TelephoneType.get("X-MMS"); + + @Getter @Setter private String unknownProperties; + + @Getter @Setter private boolean starred; + + @Getter @Setter private String displayName, nickName; + @Getter @Setter private String prefix, givenName, middleName, familyName, suffix; + @Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName; + @Getter @Setter private String note; + @Getter @Setter private Organization organization; + @Getter @Setter private String jobTitle, jobDescription; + + @Getter @Setter private byte[] photo; + + @Getter @Setter private Anniversary anniversary; + @Getter @Setter private Birthday birthDay; + + @Getter private List phoneNumbers = new LinkedList(); + @Getter private List emails = new LinkedList(); + @Getter private List impps = new LinkedList(); + @Getter private List
addresses = new LinkedList
(); + @Getter private List categories = new LinkedList(); + @Getter private List URLs = new LinkedList(); + + + /* instance methods */ + + public Contact(String name, String ETag) { + super(name, ETag); + } + + public Contact(long localID, String resourceName, String eTag) { + super(localID, resourceName, eTag); + } + + + @Override + public void initialize() { + generateUID(); + name = uid + ".vcf"; + } + + protected void generateUID() { + uid = UUID.randomUUID().toString(); + } + + + /* VCard methods */ + + @Override + public void parseEntity(InputStream is) throws IOException { + VCard vcard = Ezvcard.parse(is).first(); + if (vcard == null) + return; + + // now work through all supported properties + // supported properties are removed from the VCard after parsing + // so that only unknown properties are left and can be stored separately + + // UID + Uid uid = vcard.getUid(); + if (uid != null) { + this.uid = uid.getValue(); + vcard.removeProperties(Uid.class); + } else { + Log.w(TAG, "Received VCard without UID, generating new one"); + generateUID(); + } + + // X-DAVDROID-STARRED + RawProperty starred = vcard.getExtendedProperty(PROPERTY_STARRED); + if (starred != null && starred.getValue() != null) { + this.starred = starred.getValue().equals("1"); + vcard.removeExtendedProperty(PROPERTY_STARRED); + } else + this.starred = false; + + // FN + FormattedName fn = vcard.getFormattedName(); + if (fn != null) { + displayName = fn.getValue(); + vcard.removeProperties(FormattedName.class); + } else + Log.w(TAG, "Received invalid VCard without FN (formatted name) property"); + + // N + StructuredName n = vcard.getStructuredName(); + if (n != null) { + prefix = StringUtils.join(n.getPrefixes(), " "); + givenName = n.getGiven(); + middleName = StringUtils.join(n.getAdditional(), " "); + familyName = n.getFamily(); + suffix = StringUtils.join(n.getSuffixes(), " "); + vcard.removeProperties(StructuredName.class); + } + + // phonetic names + RawProperty + phoneticFirstName = vcard.getExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME), + phoneticMiddleName = vcard.getExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME), + phoneticLastName = vcard.getExtendedProperty(PROPERTY_PHONETIC_LAST_NAME); + if (phoneticFirstName != null) { + phoneticGivenName = phoneticFirstName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME); + } + if (phoneticMiddleName != null) { + this.phoneticMiddleName = phoneticMiddleName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME); + } + if (phoneticLastName != null) { + phoneticFamilyName = phoneticLastName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_LAST_NAME); + } + + // TEL + phoneNumbers = vcard.getTelephoneNumbers(); + vcard.removeProperties(Telephone.class); + + // EMAIL + emails = vcard.getEmails(); + vcard.removeProperties(Email.class); + + // PHOTO + for (Photo photo : vcard.getPhotos()) { + this.photo = photo.getData(); + vcard.removeProperties(Photo.class); + break; + } + + // ORG + organization = vcard.getOrganization(); + vcard.removeProperties(Organization.class); + // TITLE + for (Title title : vcard.getTitles()) { + jobTitle = title.getValue(); + vcard.removeProperties(Title.class); + break; + } + // ROLE + for (Role role : vcard.getRoles()) { + this.jobDescription = role.getValue(); + vcard.removeProperties(Role.class); + break; + } + + // IMPP + impps = vcard.getImpps(); + vcard.removeProperties(Impp.class); + + // NICKNAME + Nickname nicknames = vcard.getNickname(); + if (nicknames != null) { + if (nicknames.getValues() != null) + nickName = StringUtils.join(nicknames.getValues(), ", "); + vcard.removeProperties(Nickname.class); + } + + // NOTE + List notes = new LinkedList(); + for (Note note : vcard.getNotes()) + notes.add(note.getValue()); + if (!notes.isEmpty()) + note = StringUtils.join(notes, "\n---\n"); + vcard.removeProperties(Note.class); + + // ADR + addresses = vcard.getAddresses(); + vcard.removeProperties(Address.class); + + // CATEGORY + Categories categories = vcard.getCategories(); + if (categories != null) + this.categories = categories.getValues(); + vcard.removeProperties(Categories.class); + + // URL + for (Url url : vcard.getUrls()) + URLs.add(url.getValue()); + vcard.removeProperties(Url.class); + + // BDAY + birthDay = vcard.getBirthday(); + vcard.removeProperties(Birthday.class); + // ANNIVERSARY + anniversary = vcard.getAnniversary(); + vcard.removeProperties(Anniversary.class); + + // X-SIP + for (RawProperty sip : vcard.getExtendedProperties(PROPERTY_SIP)) + impps.add(new Impp("sip", sip.getValue())); + vcard.removeExtendedProperty(PROPERTY_SIP); + + // remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions + vcard.removeProperties(Logo.class); + vcard.removeProperties(Sound.class); + // remove properties that don't apply anymore + vcard.removeProperties(Revision.class); + vcard.removeProperties(Source.class); + // store all remaining properties into unknownProperties + if (!vcard.getProperties().isEmpty() || !vcard.getExtendedProperties().isEmpty()) + try { + unknownProperties = vcard.write(); + } catch(Exception e) { + Log.w(TAG, "Couldn't store unknown properties (maybe illegal syntax), dropping them"); + } + } + + + @Override + public ByteArrayOutputStream toEntity() throws IOException { + VCard vcard = null; + try { + if (unknownProperties != null) + vcard = Ezvcard.parse(unknownProperties).first(); + } catch (Exception e) { + Log.w(TAG, "Couldn't parse original property set, beginning from scratch"); + } + if (vcard == null) + vcard = new VCard(); + + if (uid != null) + vcard.setUid(new Uid(uid)); + else + Log.wtf(TAG, "Generating VCard without UID"); + + if (starred) + vcard.setExtendedProperty(PROPERTY_STARRED, "1"); + + if (displayName != null) + vcard.setFormattedName(displayName); + else if (organization != null && organization.getValues() != null && organization.getValues().get(0) != null) + vcard.setFormattedName(organization.getValues().get(0)); + else + Log.w(TAG, "No FN (formatted name) available to generate VCard"); + + // N + if (familyName != null || middleName != null || givenName != null) { + StructuredName n = new StructuredName(); + if (prefix != null) + for (String p : StringUtils.split(prefix)) + n.addPrefix(p); + n.setGiven(givenName); + if (middleName != null) + for (String middle : StringUtils.split(middleName)) + n.addAdditional(middle); + n.setFamily(familyName); + if (suffix != null) + for (String s : StringUtils.split(suffix)) + n.addSuffix(s); + vcard.setStructuredName(n); + } + + // phonetic names + if (phoneticGivenName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME, phoneticGivenName); + if (phoneticMiddleName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME, phoneticMiddleName); + if (phoneticFamilyName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_LAST_NAME, phoneticFamilyName); + + // TEL + for (Telephone phoneNumber : phoneNumbers) + vcard.addTelephoneNumber(phoneNumber); + + // EMAIL + for (Email email : emails) + vcard.addEmail(email); + + // ORG, TITLE, ROLE + if (organization != null) + vcard.setOrganization(organization); + if (jobTitle != null) + vcard.addTitle(jobTitle); + if (jobDescription != null) + vcard.addRole(jobDescription); + + // IMPP + for (Impp impp : impps) + vcard.addImpp(impp); + + // NICKNAME + if (!StringUtils.isBlank(nickName)) + vcard.setNickname(nickName); + + // NOTE + if (!StringUtils.isBlank(note)) + vcard.addNote(note); + + // ADR + for (Address address : addresses) + vcard.addAddress(address); + + // CATEGORY + if (!categories.isEmpty()) + vcard.setCategories(categories.toArray(new String[0])); + + // URL + for (String url : URLs) + vcard.addUrl(url); + + // ANNIVERSARY + if (anniversary != null) + vcard.setAnniversary(anniversary); + // BDAY + if (birthDay != null) + vcard.setBirthday(birthDay); + + // PHOTO + if (photo != null) + vcard.addPhoto(new Photo(photo, ImageType.JPEG)); + + // PRODID, REV + vcard.setProductId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")"); + vcard.setRevision(Revision.now()); + + // validate and print warnings + ValidationWarnings warnings = vcard.validate(VCardVersion.V3_0); + if (!warnings.isEmpty()) + Log.w(TAG, "Created potentially invalid VCard! " + warnings); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Ezvcard + .write(vcard) + .version(VCardVersion.V3_0) + .versionStrict(false) + .prodId(false) // we provide our own PRODID + .go(os); + return os; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java new file mode 100644 index 00000000..745e9b09 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * 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; + +import java.io.Closeable; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.TXTRecord; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +import android.content.Context; +import android.util.Log; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavHttpClient; +import at.bitfire.davdroid.webdav.DavIncapableException; +import at.bitfire.davdroid.webdav.HttpPropfind.Mode; +import at.bitfire.davdroid.webdav.NotAuthorizedException; +import at.bitfire.davdroid.webdav.WebDavResource; +import org.apache.http.HttpException; +import org.apache.http.impl.client.CloseableHttpClient; +import ezvcard.VCardVersion; + +public class DavResourceFinder implements Closeable { + private final static String TAG = "davdroid.DavResourceFinder"; + + protected Context context; + protected CloseableHttpClient httpClient; + + + public DavResourceFinder(Context context) { + this.context = context; + + // disable compression and enable network logging for debugging purposes + httpClient = DavHttpClient.create(true, true); + } + + @Override + public void close() throws IOException { + httpClient.close(); + } + + + public void findResources(ServerInfo serverInfo) throws URISyntaxException, DavException, HttpException, IOException { + // CardDAV + WebDavResource principal = getCurrentUserPrincipal(serverInfo, "carddav"); + if (principal != null) { + serverInfo.setCardDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathAddressBooks = principal.getAddressbookHomeSet(); + if (pathAddressBooks != null) { + Log.i(TAG, "Found address book home set: " + pathAddressBooks); + + WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks); + if (checkHomesetCapabilities(homeSetAddressBooks, "addressbook")) { + homeSetAddressBooks.propfind(Mode.CARDDAV_COLLECTIONS); + + List addressBooks = new LinkedList(); + if (homeSetAddressBooks.getMembers() != null) + for (WebDavResource resource : homeSetAddressBooks.getMembers()) + if (resource.isAddressBook()) { + Log.i(TAG, "Found address book: " + resource.getLocation().getPath()); + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + resource.isReadOnly(), + resource.getLocation().toString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + + VCardVersion version = resource.getVCardVersion(); + if (version == null) + version = VCardVersion.V3_0; // VCard 3.0 MUST be supported + info.setVCardVersion(version); + + addressBooks.add(info); + } + serverInfo.setAddressBooks(addressBooks); + } else + Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support"); + } + } + + // CalDAV + principal = getCurrentUserPrincipal(serverInfo, "caldav"); + if (principal != null) { + serverInfo.setCalDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathCalendars = principal.getCalendarHomeSet(); + if (pathCalendars != null) { + Log.i(TAG, "Found calendar home set: " + pathCalendars); + + WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars); + if (checkHomesetCapabilities(homeSetCalendars, "calendar-access")) { + homeSetCalendars.propfind(Mode.CALDAV_COLLECTIONS); + + List calendars = new LinkedList(); + if (homeSetCalendars.getMembers() != null) + for (WebDavResource resource : homeSetCalendars.getMembers()) + if (resource.isCalendar()) { + Log.i(TAG, "Found calendar: " + resource.getLocation().getPath()); + if (resource.getSupportedComponents() != null) { + // CALDAV:supported-calendar-component-set available + boolean supportsEvents = false; + for (String supportedComponent : resource.getSupportedComponents()) + if (supportedComponent.equalsIgnoreCase("VEVENT")) + supportsEvents = true; + if (!supportsEvents) { // ignore collections without VEVENT support + Log.i(TAG, "Ignoring this calendar because of missing VEVENT support"); + continue; + } + } + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.CALENDAR, + resource.isReadOnly(), + resource.getLocation().toString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + info.setTimezone(resource.getTimezone()); + calendars.add(info); + } + serverInfo.setCalendars(calendars); + } else + Log.w(TAG, "Found calendar home set, but it doesn't advertise CalDAV support"); + } + } + + if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) + throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav)); + + } + + + /** + * Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password) + * @param serverInfo User-given service information (including base URI, i.e. HTTP[S] URL+user name+password or mailto URI and password) + * @param serviceName Service name ("carddav" or "caldav") + * @return Initial service URL (HTTP/HTTPS), without user credentials + * @throws URISyntaxException when the user-given URI is invalid + * @throws MalformedURLException when the user-given URI is invalid + */ + public URI getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException, MalformedURLException { + String scheme = null, + domain = null; + int port = -1; + String path = "/"; + + URI baseURI = serverInfo.getBaseURI(); + if ("mailto".equalsIgnoreCase(baseURI.getScheme())) { + // mailto URIs + String mailbox = serverInfo.getBaseURI().getSchemeSpecificPart(); + + // determine service FQDN + int pos = mailbox.lastIndexOf("@"); + if (pos == -1) + throw new URISyntaxException(mailbox, "Missing @ sign"); + + scheme = "https"; + domain = mailbox.substring(pos + 1); + if (domain.isEmpty()) + throw new URISyntaxException(mailbox, "Missing domain name"); + } else { + // HTTP(S) URLs + scheme = baseURI.getScheme(); + domain = baseURI.getHost(); + port = baseURI.getPort(); + path = baseURI.getPath(); + } + + // try to determine FQDN and port number using SRV records + try { + String name = "_" + serviceName + "s._tcp." + domain; + Log.d(TAG, "Looking up SRV records for " + name); + Record[] records = new Lookup(name, Type.SRV).run(); + if (records != null && records.length >= 1) { + SRVRecord srv = selectSRVRecord(records); + + scheme = "https"; + domain = srv.getTarget().toString(true); + port = srv.getPort(); + Log.d(TAG, "Found " + serviceName + "s service for " + domain + " -> " + domain + ":" + port); + + if (port == 443) // no reason to explicitly give the default port + port = -1; + + // SRV record found, look for TXT record too (for initial context path) + records = new Lookup(name, Type.TXT).run(); + if (records != null && records.length >= 1) { + TXTRecord txt = (TXTRecord)records[0]; + for (Object o : txt.getStrings().toArray()) { + String segment = (String)o; + if (segment.startsWith("path=")) { + path = segment.substring(5); + Log.d(TAG, "Found initial context path for " + serviceName + " at " + domain + " -> " + path); + break; + } + } + } + } + } catch (TextParseException e) { + throw new URISyntaxException(domain, "Invalid domain name"); + } + + return new URI(scheme, null, domain, port, path, null, null); + } + + + /** + * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only + * if no current-user-principal can be detected for the .well-known location, the given location of the resource + * is tried. + * @param resource Location that will be queried + * @param serviceName Well-known service name ("carddav", "caldav") + * @return WebDavResource of current-user-principal for the given service, or null if it can't be found + * + * TODO: If a TXT record is given, always use it instead of trying .well-known first + */ + WebDavResource getCurrentUserPrincipal(ServerInfo serverInfo, String serviceName) throws URISyntaxException, IOException, NotAuthorizedException { + URI initialURL = getInitialContextURL(serverInfo, serviceName); + if (initialURL != null) { + // determine base URL (host name and initial context path) + WebDavResource base = new WebDavResource(httpClient, + initialURL, + serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.isAuthPreemptive()); + + // look for well-known service (RFC 5785) + try { + WebDavResource wellKnown = new WebDavResource(base, "/.well-known/" + serviceName); + wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (wellKnown.getCurrentUserPrincipal() != null) + return new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal()); + } catch (NotAuthorizedException e) { + Log.w(TAG, "Not authorized for well-known " + serviceName + " service detection", e); + throw e; + } catch (URISyntaxException e) { + Log.w(TAG, "Well-known" + serviceName + " service detection failed because of invalid URIs", e); + } catch (HttpException e) { + Log.d(TAG, "Well-known " + serviceName + " service detection failed with HTTP error", e); + } catch (DavException e) { + Log.w(TAG, "Well-known " + serviceName + " service detection failed with unexpected DAV response", e); + } + + // fall back to user-given initial context path + try { + base.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (base.getCurrentUserPrincipal() != null) + return new WebDavResource(base, base.getCurrentUserPrincipal()); + } catch (NotAuthorizedException e) { + Log.e(TAG, "Not authorized for querying principal", e); + throw e; + } catch (HttpException e) { + Log.e(TAG, "HTTP error when querying principal", e); + } catch (DavException e) { + Log.e(TAG, "DAV error when querying principal", e); + } + Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName); + } + return null; + } + + public static boolean checkHomesetCapabilities(WebDavResource resource, String davCapability) throws URISyntaxException, IOException { + // check for necessary capabilities + try { + resource.options(); + if (resource.supportsDAV(davCapability) && + resource.supportsMethod("PROPFIND")) // check only for methods that MUST be available for home sets + return true; + } catch(HttpException e) { + // for instance, 405 Method not allowed + } + return false; + } + + + SRVRecord selectSRVRecord(Record[] records) { + if (records.length > 1) + Log.w(TAG, "Multiple SRV records not supported yet; using first one"); + return (SRVRecord)records[0]; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Event.java b/app/src/main/java/at/bitfire/davdroid/resource/Event.java new file mode 100644 index 00000000..91f68012 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Event.java @@ -0,0 +1,368 @@ +/******************************************************************************* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.Calendar; +import java.util.LinkedList; +import java.util.List; +import java.util.SimpleTimeZone; +import java.util.TimeZone; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.CalendarOutputter; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.ComponentList; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.ValidationException; +import net.fortuna.ical4j.model.component.VAlarm; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.parameter.Value; +import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Clazz; +import net.fortuna.ical4j.model.property.DateProperty; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.DtEnd; +import net.fortuna.ical4j.model.property.DtStart; +import net.fortuna.ical4j.model.property.Duration; +import net.fortuna.ical4j.model.property.ExDate; +import net.fortuna.ical4j.model.property.ExRule; +import net.fortuna.ical4j.model.property.LastModified; +import net.fortuna.ical4j.model.property.Location; +import net.fortuna.ical4j.model.property.Organizer; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.RDate; +import net.fortuna.ical4j.model.property.RRule; +import net.fortuna.ical4j.model.property.Status; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.model.property.Transp; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.SimpleHostInfo; +import net.fortuna.ical4j.util.UidGenerator; +import android.text.format.Time; +import android.util.Log; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.syncadapter.DavSyncAdapter; + + +public class Event extends Resource { + private final static String TAG = "davdroid.Event"; + + public final static String MIME_TYPE = "text/calendar"; + + private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); + + @Getter @Setter private String summary, location, description; + + @Getter private DtStart dtStart; + @Getter private DtEnd dtEnd; + @Getter @Setter private Duration duration; + @Getter @Setter private RDate rdate; + @Getter @Setter private RRule rrule; + @Getter @Setter private ExDate exdate; + @Getter @Setter private ExRule exrule; + + @Getter @Setter private Boolean forPublic; + @Getter @Setter private Status status; + + @Getter @Setter private boolean opaque; + + @Getter @Setter private Organizer organizer; + @Getter private List attendees = new LinkedList(); + public void addAttendee(Attendee attendee) { + attendees.add(attendee); + } + + @Getter private List alarms = new LinkedList(); + public void addAlarm(VAlarm alarm) { + alarms.add(alarm); + } + + + public Event(String name, String ETag) { + super(name, ETag); + } + + public Event(long localID, String name, String ETag) { + super(localID, name, ETag); + } + + + @Override + public void initialize() { + generateUID(); + name = uid.replace("@", "_") + ".ics"; + } + + protected void generateUID() { + UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid())); + uid = generator.generateUid().getValue(); + } + + + @Override + @SuppressWarnings("unchecked") + public void parseEntity(@NonNull InputStream entity) throws IOException, InvalidResourceException { + net.fortuna.ical4j.model.Calendar ical; + try { + CalendarBuilder builder = new CalendarBuilder(); + ical = builder.build(entity); + + if (ical == null) + throw new InvalidResourceException("No iCalendar found"); + } catch (ParserException e) { + throw new InvalidResourceException(e); + } + + // event + ComponentList events = ical.getComponents(Component.VEVENT); + if (events == null || events.isEmpty()) + throw new InvalidResourceException("No VEVENT found"); + VEvent event = (VEvent)events.get(0); + + if (event.getUid() != null) + uid = event.getUid().getValue(); + else { + Log.w(TAG, "Received VEVENT without UID, generating new one"); + generateUID(); + } + + if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null) + throw new InvalidResourceException("Invalid start time/end time/duration"); + + if (hasTime(dtStart)) { + validateTimeZone(dtStart); + validateTimeZone(dtEnd); + } + + // all-day events and "events on that day": + // * related UNIX times must be in UTC + // * must have a duration (set to one day if missing) + if (!hasTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) { + Log.i(TAG, "Repairing iCal: DTEND := DTSTART+1"); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC)); + c.setTime(dtStart.getDate()); + c.add(Calendar.DATE, 1); + dtEnd.setDate(new Date(c.getTimeInMillis())); + } + + rrule = (RRule)event.getProperty(Property.RRULE); + rdate = (RDate)event.getProperty(Property.RDATE); + exrule = (ExRule)event.getProperty(Property.EXRULE); + exdate = (ExDate)event.getProperty(Property.EXDATE); + + if (event.getSummary() != null) + summary = event.getSummary().getValue(); + if (event.getLocation() != null) + location = event.getLocation().getValue(); + if (event.getDescription() != null) + description = event.getDescription().getValue(); + + status = event.getStatus(); + + opaque = true; + if (event.getTransparency() == Transp.TRANSPARENT) + opaque = false; + + organizer = event.getOrganizer(); + for (Object o : event.getProperties(Property.ATTENDEE)) + attendees.add((Attendee)o); + + Clazz classification = event.getClassification(); + if (classification != null) { + if (classification == Clazz.PUBLIC) + forPublic = true; + else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE) + forPublic = false; + } + + this.alarms = event.getAlarms(); + } + + @Override + @SuppressWarnings("unchecked") + public ByteArrayOutputStream toEntity() throws IOException { + net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar(); + ical.getProperties().add(Version.VERSION_2_0); + ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN")); + + VEvent event = new VEvent(); + PropertyList props = event.getProperties(); + + if (uid != null) + props.add(new Uid(uid)); + + props.add(dtStart); + if (dtEnd != null) + props.add(dtEnd); + if (duration != null) + props.add(duration); + + if (rrule != null) + props.add(rrule); + if (rdate != null) + props.add(rdate); + if (exrule != null) + props.add(exrule); + if (exdate != null) + props.add(exdate); + + if (summary != null && !summary.isEmpty()) + props.add(new Summary(summary)); + if (location != null && !location.isEmpty()) + props.add(new Location(location)); + if (description != null && !description.isEmpty()) + props.add(new Description(description)); + + if (status != null) + props.add(status); + if (!opaque) + props.add(Transp.TRANSPARENT); + + if (organizer != null) + props.add(organizer); + props.addAll(attendees); + + if (forPublic != null) + event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE); + + event.getAlarms().addAll(alarms); + + props.add(new LastModified()); + ical.getComponents().add(event); + + // add VTIMEZONE components + net.fortuna.ical4j.model.TimeZone + tzStart = (dtStart == null ? null : dtStart.getTimeZone()), + tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone()); + if (tzStart != null) + ical.getComponents().add(tzStart.getVTimeZone()); + if (tzEnd != null && tzEnd != tzStart) + ical.getComponents().add(tzEnd.getVTimeZone()); + + CalendarOutputter output = new CalendarOutputter(false); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + output.output(ical, os); + } catch (ValidationException e) { + Log.e(TAG, "Generated invalid iCalendar"); + } + return os; + } + + + public long getDtStartInMillis() { + return dtStart.getDate().getTime(); + } + + public String getDtStartTzID() { + return getTzId(dtStart); + } + + public void setDtStart(long tsStart, String tzID) { + if (tzID == null) { // all-day + dtStart = new DtStart(new Date(tsStart)); + } else { + DateTime start = new DateTime(tsStart); + start.setTimeZone(tzRegistry.getTimeZone(tzID)); + dtStart = new DtStart(start); + } + } + + + public long getDtEndInMillis() { + return dtEnd.getDate().getTime(); + } + + public String getDtEndTzID() { + return getTzId(dtEnd); + } + + public void setDtEnd(long tsEnd, String tzID) { + if (tzID == null) { // all-day + dtEnd = new DtEnd(new Date(tsEnd)); + } else { + DateTime end = new DateTime(tsEnd); + end.setTimeZone(tzRegistry.getTimeZone(tzID)); + dtEnd = new DtEnd(end); + } + } + + + // helpers + + public boolean isAllDay() { + return !hasTime(dtStart); + } + + protected static boolean hasTime(DateProperty date) { + return date.getDate() instanceof DateTime; + } + + protected static String getTzId(DateProperty date) { + if (date.isUtc() || !hasTime(date)) + return Time.TIMEZONE_UTC; + else if (date.getTimeZone() != null) + return date.getTimeZone().getID(); + else if (date.getParameter(Value.TZID) != null) + return date.getParameter(Value.TZID).getValue(); + + // fallback + return Time.TIMEZONE_UTC; + } + + /* guess matching Android timezone ID */ + protected static void validateTimeZone(DateProperty date) { + if (date.isUtc() || !hasTime(date)) + return; + + String tzID = getTzId(date); + if (tzID == null) + return; + + String localTZ = Time.TIMEZONE_UTC; + + String availableTZs[] = SimpleTimeZone.getAvailableIDs(); + for (String availableTZ : availableTZs) + if (tzID.indexOf(availableTZ, 0) != -1) { + localTZ = availableTZ; + break; + } + + Log.d(TAG, "Assuming time zone " + localTZ + " for " + tzID); + date.setTimeZone(tzRegistry.getTimeZone(localTZ)); + } + + public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException { + try { + if (timezoneDef != null) { + CalendarBuilder builder = new CalendarBuilder(); + net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef)); + VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE); + return timezone.getTimeZoneId().getValue(); + } + } catch (Exception ex) { + Log.w(TAG, "Can't understand time zone definition, ignoring", ex); + } + throw new IllegalArgumentException(); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java b/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java new file mode 100644 index 00000000..ee24e1f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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; + +public class InvalidResourceException extends Exception { + private static final long serialVersionUID = 1593585432655578220L; + + public InvalidResourceException(String message) { + super(message); + } + + public InvalidResourceException(Throwable throwable) { + super(throwable); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java new file mode 100644 index 00000000..b8711cbb --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java @@ -0,0 +1,1007 @@ +/******************************************************************************* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import lombok.Cleanup; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +import android.util.Log; +import at.bitfire.davdroid.syncadapter.AccountSettings; +import ezvcard.parameter.AddressType; +import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImppType; +import ezvcard.parameter.TelephoneType; +import ezvcard.property.Address; +import ezvcard.property.Anniversary; +import ezvcard.property.Birthday; +import ezvcard.property.DateOrTimeProperty; +import ezvcard.property.Impp; +import ezvcard.property.Telephone; + + +public class LocalAddressBook extends LocalCollection { + private final static String TAG = "davdroid.LocalAddressBook"; + + protected final static String COLUMN_UNKNOWN_PROPERTIES = RawContacts.SYNC3; + + + protected AccountSettings accountSettings; + + + /* database fields */ + + @Override + protected Uri entriesURI() { + return syncAdapterURI(RawContacts.CONTENT_URI); + } + + protected String entryColumnAccountType() { return RawContacts.ACCOUNT_TYPE; } + protected String entryColumnAccountName() { return RawContacts.ACCOUNT_NAME; } + + protected String entryColumnParentID() { return null; /* maybe use RawContacts.DATA_SET some day? */ } + protected String entryColumnID() { return RawContacts._ID; } + protected String entryColumnRemoteName() { return RawContacts.SOURCE_ID; } + protected String entryColumnETag() { return RawContacts.SYNC2; } + + protected String entryColumnDirty() { return RawContacts.DIRTY; } + protected String entryColumnDeleted() { return RawContacts.DELETED; } + + protected String entryColumnUID() { return RawContacts.SYNC1; } + + + + public LocalAddressBook(Account account, ContentProviderClient providerClient, AccountSettings accountSettings) { + super(account, providerClient); + this.accountSettings = accountSettings; + } + + + /* collection operations */ + + @Override + public long getId() { + return -1; + } + + @Override + public String getCTag() { + return accountSettings.getAddressBookCTag(); + } + + @Override + public void setCTag(String cTag) { + accountSettings.setAddressBookCTag(cTag); + } + + + /* create/update/delete */ + + public Contact newResource(long localID, String resourceName, String eTag) { + return new Contact(localID, resourceName, eTag); + } + + public void deleteAllExceptRemoteNames(Resource[] remoteResources) { + String where; + + if (remoteResources.length != 0) { + List sqlFileNames = new LinkedList(); + for (Resource res : remoteResources) + sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName())); + where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; + } else + where = entryColumnRemoteName() + " IS NOT NULL"; + + Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null); + pendingOperations.add(builder + .withYieldAllowed(true) + .build()); + } + + @Override + public void commit() throws LocalStorageException { + super.commit(); + + // update group details for groups we have just created + Uri groupsUri = syncAdapterURI(Groups.CONTENT_URI); + try { + // newly created groups don't have a TITLE + @Cleanup Cursor cursor = providerClient.query(groupsUri, + new String[] { Groups.SOURCE_ID }, + Groups.TITLE + " IS NULL", null, null + ); + while (cursor != null && cursor.moveToNext()) { + // found group, set TITLE to SOURCE_ID and other details + String sourceID = cursor.getString(0); + pendingOperations.add(ContentProviderOperation.newUpdate(groupsUri) + .withSelection(Groups.SOURCE_ID + "=?", new String[] { sourceID }) + .withValue(Groups.TITLE, sourceID) + .withValue(Groups.GROUP_VISIBLE, 1) + .build()); + super.commit(); + } + } catch (RemoteException e) { + throw new LocalStorageException("Couldn't update group names", e); + } + } + + + /* methods for populating the data object from the content provider */ + + @Override + public void populate(Resource res) throws LocalStorageException { + Contact c = (Contact)res; + + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()), + new String[] { entryColumnUID(), COLUMN_UNKNOWN_PROPERTIES, RawContacts.STARRED }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + c.setUid(cursor.getString(0)); + c.setUnknownProperties(cursor.getString(1)); + c.setStarred(cursor.getInt(2) != 0); + } else + throw new RecordNotFoundException(); + + populateStructuredName(c); + populatePhoneNumbers(c); + populateEmailAddresses(c); + populatePhoto(c); + populateOrganization(c); + populateIMPPs(c); + populateNickname(c); + populateNote(c); + populatePostalAddresses(c); + populateCategories(c); + populateURLs(c); + populateEvents(c); + populateSipAddress(c); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + private void populateStructuredName(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { + /* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME, + /* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX, + /* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME + }, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null); + + if (cursor != null && cursor.moveToNext()) { + c.setDisplayName(cursor.getString(0)); + + c.setPrefix(cursor.getString(1)); + c.setGivenName(cursor.getString(2)); + c.setMiddleName(cursor.getString(3)); + c.setFamilyName(cursor.getString(4)); + c.setSuffix(cursor.getString(5)); + + c.setPhoneticGivenName(cursor.getString(6)); + c.setPhoneticMiddleName(cursor.getString(7)); + c.setPhoneticFamilyName(cursor.getString(8)); + } + } + + protected void populatePhoneNumbers(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER, Phone.IS_SUPER_PRIMARY }, + Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + ezvcard.property.Telephone number = new ezvcard.property.Telephone(cursor.getString(2)); + switch (cursor.getInt(0)) { + case Phone.TYPE_HOME: + number.addType(TelephoneType.HOME); + break; + case Phone.TYPE_MOBILE: + number.addType(TelephoneType.CELL); + break; + case Phone.TYPE_WORK: + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_FAX_WORK: + number.addType(TelephoneType.FAX); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_FAX_HOME: + number.addType(TelephoneType.FAX); + number.addType(TelephoneType.HOME); + break; + case Phone.TYPE_PAGER: + number.addType(TelephoneType.PAGER); + break; + case Phone.TYPE_CALLBACK: + number.addType(Contact.PHONE_TYPE_CALLBACK); + break; + case Phone.TYPE_CAR: + number.addType(TelephoneType.CAR); + break; + case Phone.TYPE_COMPANY_MAIN: + number.addType(Contact.PHONE_TYPE_COMPANY_MAIN); + break; + case Phone.TYPE_ISDN: + number.addType(TelephoneType.ISDN); + break; + case Phone.TYPE_MAIN: + number.addType(TelephoneType.PREF); + break; + case Phone.TYPE_OTHER_FAX: + number.addType(TelephoneType.FAX); + break; + case Phone.TYPE_RADIO: + number.addType(Contact.PHONE_TYPE_RADIO); + break; + case Phone.TYPE_TELEX: + number.addType(TelephoneType.TEXTPHONE); + break; + case Phone.TYPE_TTY_TDD: + number.addType(TelephoneType.TEXT); + break; + case Phone.TYPE_WORK_MOBILE: + number.addType(TelephoneType.CELL); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_WORK_PAGER: + number.addType(TelephoneType.PAGER); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_ASSISTANT: + number.addType(Contact.PHONE_TYPE_ASSISTANT); + break; + case Phone.TYPE_MMS: + number.addType(Contact.PHONE_TYPE_MMS); + break; + case Phone.TYPE_CUSTOM: + String customType = cursor.getString(1); + if (!StringUtils.isEmpty(customType)) + number.addType(TelephoneType.get(labelToXName(customType))); + } + if (cursor.getInt(3) != 0) // IS_PRIMARY + number.addType(TelephoneType.PREF); + c.getPhoneNumbers().add(number); + } + } + + protected void populateEmailAddresses(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL, Email.IS_SUPER_PRIMARY }, + Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + ezvcard.property.Email email = new ezvcard.property.Email(cursor.getString(1)); + switch (cursor.getInt(0)) { + case Email.TYPE_HOME: + email.addType(EmailType.HOME); + break; + case Email.TYPE_WORK: + email.addType(EmailType.WORK); + break; + case Email.TYPE_MOBILE: + email.addType(Contact.EMAIL_TYPE_MOBILE); + break; + case Email.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + email.addType(EmailType.get(labelToXName(customType))); + } + if (cursor.getInt(3) != 0) // IS_PRIMARY + email.addType(EmailType.PREF); + c.getEmails().add(email); + } + } + + protected void populatePhoto(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { Photo.PHOTO_FILE_ID, Photo.PHOTO }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + if (!cursor.isNull(0)) { + Uri photoUri = Uri.withAppendedPath( + ContentUris.withAppendedId(RawContacts.CONTENT_URI, c.getLocalID()), + RawContacts.DisplayPhoto.CONTENT_DIRECTORY); + try { + @Cleanup AssetFileDescriptor fd = providerClient.openAssetFile(photoUri, "r"); + @Cleanup InputStream is = fd.createInputStream(); + c.setPhoto(IOUtils.toByteArray(is)); + } catch(IOException ex) { + Log.w(TAG, "Couldn't read high-res contact photo", ex); + } + } else + c.setPhoto(cursor.getBlob(1)); + } + } + + protected void populateOrganization(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { Organization.COMPANY, Organization.DEPARTMENT, Organization.TITLE, Organization.JOB_DESCRIPTION }, + Organization.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + String company = cursor.getString(0), + department = cursor.getString(1), + title = cursor.getString(2), + role = cursor.getString(3); + if (!StringUtils.isEmpty(company) || !StringUtils.isEmpty(department)) { + ezvcard.property.Organization org = new ezvcard.property.Organization(); + if (!StringUtils.isEmpty(company)) + org.addValue(company); + if (!StringUtils.isEmpty(department)) + org.addValue(department); + c.setOrganization(org); + } + if (!StringUtils.isEmpty(title)) + c.setJobTitle(title); + if (!StringUtils.isEmpty(role)) + c.setJobDescription(role); + } + } + + protected void populateIMPPs(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL }, + Im.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + String handle = cursor.getString(0); + + Impp impp = null; + switch (cursor.getInt(3)) { + case Im.PROTOCOL_AIM: + impp = Impp.aim(handle); + break; + case Im.PROTOCOL_MSN: + impp = Impp.msn(handle); + break; + case Im.PROTOCOL_YAHOO: + impp = Impp.yahoo(handle); + break; + case Im.PROTOCOL_SKYPE: + impp = Impp.skype(handle); + break; + case Im.PROTOCOL_QQ: + impp = new Impp("qq", handle); + break; + case Im.PROTOCOL_GOOGLE_TALK: + impp = new Impp("google-talk", handle); + break; + case Im.PROTOCOL_ICQ: + impp = Impp.icq(handle); + break; + case Im.PROTOCOL_JABBER: + impp = Impp.xmpp(handle); + break; + case Im.PROTOCOL_NETMEETING: + impp = new Impp("netmeeting", handle); + break; + case Im.PROTOCOL_CUSTOM: + impp = new Impp(cursor.getString(4), handle); + } + + if (impp != null) { + switch (cursor.getInt(1)) { + case Im.TYPE_HOME: + impp.addType(ImppType.HOME); + break; + case Im.TYPE_WORK: + impp.addType(ImppType.WORK); + break; + case Im.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + impp.addType(ImppType.get(labelToXName(customType))); + } + c.getImpps().add(impp); + } + } + } + + protected void populateNickname(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME }, + Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Nickname.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) + c.setNickName(cursor.getString(0)); + } + + protected void populateNote(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Note.NOTE }, + Note.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Note.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) + c.setNote(cursor.getString(0)); + } + + protected void populatePostalAddresses(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { + /* 0 */ StructuredPostal.FORMATTED_ADDRESS, StructuredPostal.TYPE, StructuredPostal.LABEL, + /* 3 */ StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, + /* 6 */ StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, + /* 9 */ StructuredPostal.COUNTRY + }, StructuredPostal.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), StructuredPostal.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + Address address = new Address(); + + address.setLabel(cursor.getString(0)); + switch (cursor.getInt(1)) { + case StructuredPostal.TYPE_HOME: + address.addType(AddressType.HOME); + break; + case StructuredPostal.TYPE_WORK: + address.addType(AddressType.WORK); + break; + case StructuredPostal.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + address.addType(AddressType.get(labelToXName(customType))); + break; + } + address.setStreetAddress(cursor.getString(3)); + address.setPoBox(cursor.getString(4)); + address.setExtendedAddress(cursor.getString(5)); + address.setLocality(cursor.getString(6)); + address.setRegion(cursor.getString(7)); + address.setPostalCode(cursor.getString(8)); + address.setCountry(cursor.getString(9)); + c.getAddresses().add(address); + } + } + + protected void populateCategories(Contact c) throws RemoteException { + @Cleanup Cursor cursorMemberships = providerClient.query(dataURI(), + new String[] { GroupMembership.GROUP_ROW_ID, GroupMembership.GROUP_SOURCE_ID }, + GroupMembership.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), GroupMembership.CONTENT_ITEM_TYPE }, null); + List categories = c.getCategories(); + while (cursorMemberships != null && cursorMemberships.moveToNext()) { + long rowID = cursorMemberships.getLong(0); + String sourceID = cursorMemberships.getString(1); + + // either a row ID or a source ID must be available + String where, whereArg; + if (sourceID == null) { + where = Groups._ID + "=?"; + whereArg = String.valueOf(rowID); + } else { + where = Groups.SOURCE_ID + "=?"; + whereArg = sourceID; + } + where += " AND " + Groups.DELETED + "=0"; // ignore deleted groups + Log.d(TAG, "Populating group from " + where + " " + whereArg); + + // fetch group + @Cleanup Cursor cursorGroups = providerClient.query(Groups.CONTENT_URI, + new String[] { Groups.TITLE }, + where, new String[] { whereArg }, null + ); + if (cursorGroups != null && cursorGroups.moveToNext()) { + String title = cursorGroups.getString(0); + + if (sourceID == null) { // Group wasn't created by DAVdroid + // SOURCE_ID IS NULL <=> _ID IS NOT NULL + Log.d(TAG, "Setting SOURCE_ID of non-DAVdroid group to title: " + title); + + ContentValues v = new ContentValues(1); + v.put(Groups.SOURCE_ID, title); + v.put(Groups.GROUP_IS_READ_ONLY, 0); + v.put(Groups.GROUP_VISIBLE, 1); + providerClient.update(syncAdapterURI(Groups.CONTENT_URI), v, Groups._ID + "=?", new String[] { String.valueOf(rowID) }); + + sourceID = title; + } + + // add group to CATEGORIES + if (sourceID != null) + categories.add(sourceID); + } else + Log.d(TAG, "Group not found (maybe deleted)"); + } + } + + protected void populateURLs(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Website.URL }, + Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Website.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) + c.getURLs().add(cursor.getString(0)); + } + + protected void populateEvents(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), CommonDataKinds.Event.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + try { + Date date = formatter.parse(cursor.getString(1)); + switch (cursor.getInt(0)) { + case CommonDataKinds.Event.TYPE_ANNIVERSARY: + c.setAnniversary(new Anniversary(date)); + break; + case CommonDataKinds.Event.TYPE_BIRTHDAY: + c.setBirthDay(new Birthday(date)); + break; + } + } catch (ParseException e) { + Log.w(TAG, "Couldn't parse local birthday/anniversary date", e); + } + } + } + + protected void populateSipAddress(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { SipAddress.SIP_ADDRESS, SipAddress.TYPE, SipAddress.LABEL }, + SipAddress.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), SipAddress.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + Impp impp = new Impp("sip:" + cursor.getString(0)); + switch (cursor.getInt(1)) { + case SipAddress.TYPE_HOME: + impp.addType(ImppType.HOME); + break; + case SipAddress.TYPE_WORK: + impp.addType(ImppType.WORK); + break; + case SipAddress.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + impp.addType(ImppType.get(labelToXName(customType))); + } + c.getImpps().add(impp); + } + } + + + /* content builder methods */ + + @Override + protected Builder buildEntry(Builder builder, Resource resource) { + Contact contact = (Contact)resource; + + return builder + .withValue(RawContacts.ACCOUNT_NAME, account.name) + .withValue(RawContacts.ACCOUNT_TYPE, account.type) + .withValue(entryColumnRemoteName(), contact.getName()) + .withValue(entryColumnUID(), contact.getUid()) + .withValue(entryColumnETag(), contact.getETag()) + .withValue(COLUMN_UNKNOWN_PROPERTIES, contact.getUnknownProperties()) + .withValue(RawContacts.STARRED, contact.isStarred() ? 1 : 0); + } + + + @Override + protected void addDataRows(Resource resource, long localID, int backrefIdx) { + Contact contact = (Contact)resource; + + queueOperation(buildStructuredName(newDataInsertBuilder(localID, backrefIdx), contact)); + + for (Telephone number : contact.getPhoneNumbers()) + queueOperation(buildPhoneNumber(newDataInsertBuilder(localID, backrefIdx), number)); + + for (ezvcard.property.Email email : contact.getEmails()) + queueOperation(buildEmail(newDataInsertBuilder(localID, backrefIdx), email)); + + if (contact.getPhoto() != null) + queueOperation(buildPhoto(newDataInsertBuilder(localID, backrefIdx), contact.getPhoto())); + + queueOperation(buildOrganization(newDataInsertBuilder(localID, backrefIdx), contact)); + + for (Impp impp : contact.getImpps()) + queueOperation(buildIMPP(newDataInsertBuilder(localID, backrefIdx), impp)); + + if (contact.getNickName() != null) + queueOperation(buildNickName(newDataInsertBuilder(localID, backrefIdx), contact.getNickName())); + + if (contact.getNote() != null) + queueOperation(buildNote(newDataInsertBuilder(localID, backrefIdx), contact.getNote())); + + for (Address address : contact.getAddresses()) + queueOperation(buildAddress(newDataInsertBuilder(localID, backrefIdx), address)); + + for (String category : contact.getCategories()) + queueOperation(buildGroupMembership(newDataInsertBuilder(localID, backrefIdx), category)); + + for (String url : contact.getURLs()) + queueOperation(buildURL(newDataInsertBuilder(localID, backrefIdx), url)); + + // events + if (contact.getAnniversary() != null) + queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.getAnniversary(), CommonDataKinds.Event.TYPE_ANNIVERSARY)); + if (contact.getBirthDay() != null) + queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.getBirthDay(), CommonDataKinds.Event.TYPE_BIRTHDAY)); + + // TODO relations + + // SIP addresses are built by buildIMPP + } + + @Override + protected void removeDataRows(Resource resource) { + pendingOperations.add(ContentProviderOperation.newDelete(dataURI()) + .withSelection(Data.RAW_CONTACT_ID + "=?", + new String[] { String.valueOf(resource.getLocalID()) }).build()); + } + + + protected Builder buildStructuredName(Builder builder, Contact contact) { + return builder + .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + .withValue(StructuredName.PREFIX, contact.getPrefix()) + .withValue(StructuredName.DISPLAY_NAME, contact.getDisplayName()) + .withValue(StructuredName.GIVEN_NAME, contact.getGivenName()) + .withValue(StructuredName.MIDDLE_NAME, contact.getMiddleName()) + .withValue(StructuredName.FAMILY_NAME, contact.getFamilyName()) + .withValue(StructuredName.SUFFIX, contact.getSuffix()) + .withValue(StructuredName.PHONETIC_GIVEN_NAME, contact.getPhoneticGivenName()) + .withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.getPhoneticMiddleName()) + .withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.getPhoneticFamilyName()); + } + + protected Builder buildPhoneNumber(Builder builder, Telephone number) { + int typeCode = Phone.TYPE_OTHER; + String typeLabel = null; + boolean is_primary = false; + + Set types = number.getTypes(); + // preferred number? + if (types.contains(TelephoneType.PREF)) + is_primary = true; + + // 1 Android type <-> 2 VCard types: fax, cell, pager + if (types.contains(TelephoneType.FAX)) { + if (types.contains(TelephoneType.HOME)) + typeCode = Phone.TYPE_FAX_HOME; + else if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_FAX_WORK; + else + typeCode = Phone.TYPE_OTHER_FAX; + } else if (types.contains(TelephoneType.CELL)) { + if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_WORK_MOBILE; + else + typeCode = Phone.TYPE_MOBILE; + } else if (types.contains(TelephoneType.PAGER)) { + if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_WORK_PAGER; + else + typeCode = Phone.TYPE_PAGER; + // types with 1:1 translation + } else if (types.contains(TelephoneType.HOME)) { + typeCode = Phone.TYPE_HOME; + } else if (types.contains(TelephoneType.WORK)) { + typeCode = Phone.TYPE_WORK; + } else if (types.contains(Contact.PHONE_TYPE_CALLBACK)) { + typeCode = Phone.TYPE_CALLBACK; + } else if (types.contains(TelephoneType.CAR)) { + typeCode = Phone.TYPE_CAR; + } else if (types.contains(Contact.PHONE_TYPE_COMPANY_MAIN)) { + typeCode = Phone.TYPE_COMPANY_MAIN; + } else if (types.contains(TelephoneType.ISDN)) { + typeCode = Phone.TYPE_ISDN; + } else if (types.contains(TelephoneType.PREF)) { + typeCode = Phone.TYPE_MAIN; + } else if (types.contains(Contact.PHONE_TYPE_RADIO)) { + typeCode = Phone.TYPE_RADIO; + } else if (types.contains(TelephoneType.TEXTPHONE)) { + typeCode = Phone.TYPE_TELEX; + } else if (types.contains(TelephoneType.TEXT)) { + typeCode = Phone.TYPE_TTY_TDD; + } else if (types.contains(Contact.PHONE_TYPE_ASSISTANT)) { + typeCode = Phone.TYPE_ASSISTANT; + } else if (types.contains(Contact.PHONE_TYPE_MMS)) { + typeCode = Phone.TYPE_MMS; + } else if (!types.isEmpty()) { + TelephoneType type = types.iterator().next(); + typeCode = Phone.TYPE_CUSTOM; + typeLabel = xNameToLabel(type.getValue()); + } + + builder = builder + .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE) + .withValue(Phone.NUMBER, number.getText()) + .withValue(Phone.TYPE, typeCode) + .withValue(Phone.IS_PRIMARY, is_primary ? 1 : 0) + .withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0); + if (typeLabel != null) + builder = builder.withValue(Phone.LABEL, typeLabel); + return builder; + } + + protected Builder buildEmail(Builder builder, ezvcard.property.Email email) { + int typeCode = 0; + String typeLabel = null; + boolean is_primary = false; + + for (EmailType type : email.getTypes()) + if (type == EmailType.PREF) + is_primary = true; + else if (type == EmailType.HOME) + typeCode = Email.TYPE_HOME; + else if (type == EmailType.WORK) + typeCode = Email.TYPE_WORK; + else if (type == Contact.EMAIL_TYPE_MOBILE) + typeCode = Email.TYPE_MOBILE; + if (typeCode == 0) { + if (email.getTypes().isEmpty()) + typeCode = Email.TYPE_OTHER; + else { + typeCode = Email.TYPE_CUSTOM; + typeLabel = xNameToLabel(email.getTypes().iterator().next().getValue()); + } + } + + builder = builder + .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE) + .withValue(Email.ADDRESS, email.getValue()) + .withValue(Email.TYPE, typeCode) + .withValue(Email.IS_PRIMARY, is_primary ? 1 : 0) + .withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);; + if (typeLabel != null) + builder = builder.withValue(Email.LABEL, typeLabel); + return builder; + } + + protected Builder buildPhoto(Builder builder, byte[] photo) { + return builder + .withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE) + .withValue(Photo.PHOTO, photo); + } + + protected Builder buildOrganization(Builder builder, Contact contact) { + if (contact.getOrganization() == null && contact.getJobTitle() == null && contact.getJobDescription() == null) + return null; + + ezvcard.property.Organization organization = contact.getOrganization(); + String company = null, department = null; + if (organization != null) { + Iterator org = organization.getValues().iterator(); + if (org.hasNext()) + company = org.next(); + if (org.hasNext()) + department = org.next(); + } + + return builder + .withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE) + .withValue(Organization.COMPANY, company) + .withValue(Organization.DEPARTMENT, department) + .withValue(Organization.TITLE, contact.getJobTitle()) + .withValue(Organization.JOB_DESCRIPTION, contact.getJobDescription()); + } + + protected Builder buildIMPP(Builder builder, Impp impp) { + int typeCode = 0; + String typeLabel = null; + for (ImppType type : impp.getTypes()) + if (type == ImppType.HOME) + typeCode = Im.TYPE_HOME; + else if (type == ImppType.WORK || type == ImppType.BUSINESS) + typeCode = Im.TYPE_WORK; + if (typeCode == 0) + if (impp.getTypes().isEmpty()) + typeCode = Im.TYPE_OTHER; + else { + typeCode = Im.TYPE_CUSTOM; + typeLabel = xNameToLabel(impp.getTypes().iterator().next().getValue()); + } + + int protocolCode = 0; + String protocolLabel = null; + + String protocol = impp.getProtocol(); + if (protocol == null) { + Log.w(TAG, "Ignoring IMPP address without protocol"); + return null; + } + + // SIP addresses are IMPP entries in the VCard but locally stored in SipAddress rather than Im + boolean sipAddress = false; + + if (impp.isAim()) + protocolCode = Im.PROTOCOL_AIM; + else if (impp.isMsn()) + protocolCode = Im.PROTOCOL_MSN; + else if (impp.isYahoo()) + protocolCode = Im.PROTOCOL_YAHOO; + else if (impp.isSkype()) + protocolCode = Im.PROTOCOL_SKYPE; + else if (protocol.equalsIgnoreCase("qq")) + protocolCode = Im.PROTOCOL_QQ; + else if (protocol.equalsIgnoreCase("google-talk")) + protocolCode = Im.PROTOCOL_GOOGLE_TALK; + else if (impp.isIcq()) + protocolCode = Im.PROTOCOL_ICQ; + else if (impp.isXmpp() || protocol.equalsIgnoreCase("jabber")) + protocolCode = Im.PROTOCOL_JABBER; + else if (protocol.equalsIgnoreCase("netmeeting")) + protocolCode = Im.PROTOCOL_NETMEETING; + else if (protocol.equalsIgnoreCase("sip")) + sipAddress = true; + else { + protocolCode = Im.PROTOCOL_CUSTOM; + protocolLabel = protocol; + } + + if (sipAddress) + // save as SIP address + builder = builder + .withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE) + .withValue(Im.DATA, impp.getHandle()) + .withValue(Im.TYPE, typeCode); + else { + // save as IM address + builder = builder + .withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE) + .withValue(Im.DATA, impp.getHandle()) + .withValue(Im.TYPE, typeCode) + .withValue(Im.PROTOCOL, protocolCode); + if (protocolLabel != null) + builder = builder.withValue(Im.CUSTOM_PROTOCOL, protocolLabel); + } + if (typeLabel != null) + builder = builder.withValue(Im.LABEL, typeLabel); + return builder; + } + + protected Builder buildNickName(Builder builder, String nickName) { + return builder + .withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) + .withValue(Nickname.NAME, nickName); + } + + protected Builder buildNote(Builder builder, String note) { + return builder + .withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) + .withValue(Note.NOTE, note); + } + + protected Builder buildAddress(Builder builder, Address address) { + /* street po.box (extended) + * region + * postal code city + * country + */ + String formattedAddress = address.getLabel(); + if (StringUtils.isEmpty(formattedAddress)) { + String lineStreet = StringUtils.join(new String[] { address.getStreetAddress(), address.getPoBox(), address.getExtendedAddress() }, " "), + lineLocality = StringUtils.join(new String[] { address.getPostalCode(), address.getLocality() }, " "); + + List lines = new LinkedList(); + if (lineStreet != null) + lines.add(lineStreet); + if (address.getRegion() != null && !address.getRegion().isEmpty()) + lines.add(address.getRegion()); + if (lineLocality != null) + lines.add(lineLocality); + + formattedAddress = StringUtils.join(lines, "\n"); + } + + int typeCode = 0; + String typeLabel = null; + for (AddressType type : address.getTypes()) + if (type == AddressType.HOME) + typeCode = StructuredPostal.TYPE_HOME; + else if (type == AddressType.WORK) + typeCode = StructuredPostal.TYPE_WORK; + if (typeCode == 0) + if (address.getTypes().isEmpty()) + typeCode = StructuredPostal.TYPE_OTHER; + else { + typeCode = StructuredPostal.TYPE_CUSTOM; + typeLabel = xNameToLabel(address.getTypes().iterator().next().getValue()); + } + + builder = builder + .withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(StructuredPostal.FORMATTED_ADDRESS, formattedAddress) + .withValue(StructuredPostal.TYPE, typeCode) + .withValue(StructuredPostal.STREET, address.getStreetAddress()) + .withValue(StructuredPostal.POBOX, address.getPoBox()) + .withValue(StructuredPostal.NEIGHBORHOOD, address.getExtendedAddress()) + .withValue(StructuredPostal.CITY, address.getLocality()) + .withValue(StructuredPostal.REGION, address.getRegion()) + .withValue(StructuredPostal.POSTCODE, address.getPostalCode()) + .withValue(StructuredPostal.COUNTRY, address.getCountry()); + if (typeLabel != null) + builder = builder.withValue(StructuredPostal.LABEL, typeLabel); + return builder; + } + + protected Builder buildGroupMembership(Builder builder, String group) { + return builder + .withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + .withValue(GroupMembership.GROUP_SOURCE_ID, group); + } + + protected Builder buildURL(Builder builder, String url) { + return builder + .withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE) + .withValue(Website.URL, url); + } + + protected Builder buildEvent(Builder builder, DateOrTimeProperty date, int type) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + if (date.getDate() == null) { + Log.i(TAG, "Ignoring contact event without date"); + return null; + } + return builder + .withValue(Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Event.TYPE, type) + .withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate())); + } + + + + /* helper methods */ + + protected Uri dataURI() { + return syncAdapterURI(Data.CONTENT_URI); + } + + protected static String labelToXName(String label) { + return "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US); + } + + private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) { + return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx); + } + + protected static String xNameToLabel(String xname) { + // "X-MY_PROPERTY" + // 1. ensure lower case -> "x-my_property" + // 2. remove x- from beginning -> "my_property" + // 3. replace "_" by " " -> "my property" + // 4. capitalize -> "My Property" + String lowerCase = StringUtils.lowerCase(xname, Locale.US), + withoutPrefix = StringUtils.removeStart(lowerCase, "x-"), + withSpaces = StringUtils.replace(withoutPrefix, "_", " "); + return WordUtils.capitalize(withSpaces); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java new file mode 100644 index 00000000..9fb60064 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java @@ -0,0 +1,612 @@ +/******************************************************************************* + * 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; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Cleanup; +import lombok.Getter; +import net.fortuna.ical4j.model.Dur; +import net.fortuna.ical4j.model.Parameter; +import net.fortuna.ical4j.model.ParameterList; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.component.VAlarm; +import net.fortuna.ical4j.model.parameter.Cn; +import net.fortuna.ical4j.model.parameter.CuType; +import net.fortuna.ical4j.model.parameter.PartStat; +import net.fortuna.ical4j.model.parameter.Role; +import net.fortuna.ical4j.model.property.Action; +import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Duration; +import net.fortuna.ical4j.model.property.ExDate; +import net.fortuna.ical4j.model.property.ExRule; +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.annotation.TargetApi; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +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.provider.ContactsContract; +import android.util.Log; + +/** + * Represents a locally stored calendar, containing Events. + * Communicates with the Android Contacts Provider which uses an SQLite + * database to store the contacts. + */ +public class LocalCalendar extends LocalCollection { + private static final String TAG = "davdroid.LocalCalendar"; + + @Getter protected long id; + @Getter protected String url; + + protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1; + + + /* database fields */ + + @Override + protected Uri entriesURI() { + return syncAdapterURI(Events.CONTENT_URI); + } + + protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; } + protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; } + + protected String entryColumnParentID() { return Events.CALENDAR_ID; } + protected String entryColumnID() { return Events._ID; } + protected String entryColumnRemoteName() { return Events._SYNC_ID; } + protected String entryColumnETag() { return Events.SYNC_DATA1; } + + protected String entryColumnDirty() { return Events.DIRTY; } + protected String entryColumnDeleted() { return Events.DELETED; } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected String entryColumnUID() { + return (android.os.Build.VERSION.SDK_INT >= 17) ? + Events.UID_2445 : Events.SYNC_DATA2; + } + + + /* class methods, constructor */ + + @SuppressLint("InlinedApi") + public static void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException { + ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY); + if (client == null) + throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)"); + + int color = 0xFFC3EA6E; // fallback: "DAVdroid green" + if (info.getColor() != null) { + Pattern p = Pattern.compile("#?(\\p{XDigit}{6})(\\p{XDigit}{2})?"); + Matcher m = p.matcher(info.getColor()); + if (m.find()) { + int color_rgb = Integer.parseInt(m.group(1), 16); + int color_alpha = m.group(2) != null ? (Integer.parseInt(m.group(2), 16) & 0xFF) : 0xFF; + color = (color_alpha << 24) | color_rgb; + } + } + + ContentValues values = new ContentValues(); + values.put(Calendars.ACCOUNT_NAME, account.name); + values.put(Calendars.ACCOUNT_TYPE, account.type); + values.put(Calendars.NAME, info.getURL()); + values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle()); + values.put(Calendars.CALENDAR_COLOR, color); + values.put(Calendars.OWNER_ACCOUNT, account.name); + values.put(Calendars.SYNC_EVENTS, 1); + values.put(Calendars.VISIBLE, 1); + values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); + + if (info.isReadOnly()) + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ); + else { + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER); + values.put(Calendars.CAN_ORGANIZER_RESPOND, 1); + values.put(Calendars.CAN_MODIFY_TIME_ZONE, 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); + } + + if (info.getTimezone() != null) + values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone()); + + Log.i(TAG, "Inserting calendar: " + values.toString() + " -> " + calendarsURI(account).toString()); + try { + client.insert(calendarsURI(account), values); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(calendarsURI(account), + new String[] { Calendars._ID, Calendars.NAME }, + Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null); + + LinkedList calendars = new LinkedList(); + while (cursor != null && cursor.moveToNext()) + calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1))); + return calendars.toArray(new LocalCalendar[0]); + } + + public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) throws RemoteException { + super(account, providerClient); + this.id = id; + this.url = url; + } + + + /* collection operations */ + + @Override + public String getCTag() throws LocalStorageException { + try { + @Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id), + new String[] { COLLECTION_COLUMN_CTAG }, null, null, null); + if (c.moveToFirst()) { + return c.getString(0); + } else + throw new LocalStorageException("Couldn't query calendar CTag"); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + @Override + public void setCTag(String cTag) throws LocalStorageException { + ContentValues values = new ContentValues(1); + values.put(COLLECTION_COLUMN_CTAG, cTag); + try { + providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + + /* create/update/delete */ + + public Event newResource(long localID, String resourceName, String eTag) { + return new Event(localID, resourceName, eTag); + } + + public void deleteAllExceptRemoteNames(Resource[] remoteResources) { + String where; + + if (remoteResources.length != 0) { + List sqlFileNames = new LinkedList(); + for (Resource res : remoteResources) + sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName())); + where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; + } else + where = entryColumnRemoteName() + " IS NOT NULL"; + + Builder builder = ContentProviderOperation.newDelete(entriesURI()) + .withSelection(entryColumnParentID() + "=? AND (" + where + ")", new String[] { String.valueOf(id) }); + pendingOperations.add(builder + .withYieldAllowed(true) + .build()); + } + + + /* methods for populating the data object from the content provider */ + + + @Override + public void populate(Resource resource) throws LocalStorageException { + Event e = (Event)resource; + + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()), + new String[] { + /* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION, + /* 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, + /* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY + }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + e.setUid(cursor.getString(17)); + + e.setSummary(cursor.getString(0)); + e.setLocation(cursor.getString(1)); + e.setDescription(cursor.getString(2)); + + boolean allDay = cursor.getInt(7) != 0; + long tsStart = cursor.getLong(3), + tsEnd = cursor.getLong(4); + String duration = cursor.getString(18); + + String tzId = null; + if (allDay) { + e.setDtStart(tsStart, null); + // provide only DTEND and not DURATION for all-day events + if (tsEnd == 0) { + Dur dur = new Dur(duration); + java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart)); + tsEnd = dEnd.getTime(); + } + e.setDtEnd(tsEnd, null); + + } else { + // use the start time zone for the end time, too + // because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only + tzId = cursor.getString(5); + e.setDtStart(tsStart, tzId); + if (tsEnd != 0) + e.setDtEnd(tsEnd, tzId); + else if (!StringUtils.isEmpty(duration)) + e.setDuration(new Duration(new Dur(duration))); + } + + // recurrence + try { + String strRRule = cursor.getString(10); + if (!StringUtils.isEmpty(strRRule)) + e.setRrule(new RRule(strRRule)); + + String strRDate = cursor.getString(11); + if (!StringUtils.isEmpty(strRDate)) { + RDate rDate = new RDate(); + rDate.setValue(strRDate); + e.setRdate(rDate); + } + + String strExRule = cursor.getString(12); + if (!StringUtils.isEmpty(strExRule)) { + ExRule exRule = new ExRule(); + exRule.setValue(strExRule); + e.setExrule(exRule); + } + + String strExDate = cursor.getString(13); + if (!StringUtils.isEmpty(strExDate)) { + // ignored, see https://code.google.com/p/android/issues/detail?id=21426 + ExDate exDate = new ExDate(); + exDate.setValue(strExDate); + e.setExdate(exDate); + } + } catch (ParseException ex) { + Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Invalid recurrence rules, ignoring", ex); + } + + // status + switch (cursor.getInt(8)) { + case Events.STATUS_CONFIRMED: + e.setStatus(Status.VEVENT_CONFIRMED); + break; + case Events.STATUS_TENTATIVE: + e.setStatus(Status.VEVENT_TENTATIVE); + break; + case Events.STATUS_CANCELED: + e.setStatus(Status.VEVENT_CANCELLED); + } + + // availability + e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE); + + // attendees + if (cursor.getInt(14) != 0) { // has attendees + try { + e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null))); + } catch (URISyntaxException ex) { + Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex); + } + populateAttendees(e); + } + + // classification + switch (cursor.getInt(9)) { + case Events.ACCESS_CONFIDENTIAL: + case Events.ACCESS_PRIVATE: + e.setForPublic(false); + break; + case Events.ACCESS_PUBLIC: + e.setForPublic(true); + } + + populateReminders(e); + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + + void populateAttendees(Event e) throws RemoteException { + Uri attendeesUri = Attendees.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + @Cleanup Cursor c = providerClient.query(attendeesUri, new String[] { + /* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE, + /* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS + }, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + while (c != null && c.moveToNext()) { + try { + Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null)); + ParameterList params = attendee.getParameters(); + + String cn = c.getString(1); + if (cn != null) + params.add(new Cn(cn)); + + // type + int type = c.getInt(2); + params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL); + + // role + int relationship = c.getInt(3); + switch (relationship) { + case Attendees.RELATIONSHIP_ORGANIZER: + params.add(Role.CHAIR); + break; + case Attendees.RELATIONSHIP_ATTENDEE: + case Attendees.RELATIONSHIP_PERFORMER: + case Attendees.RELATIONSHIP_SPEAKER: + params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT); + break; + case Attendees.RELATIONSHIP_NONE: + params.add(Role.NON_PARTICIPANT); + } + + // status + switch (c.getInt(4)) { + case Attendees.ATTENDEE_STATUS_INVITED: + params.add(PartStat.NEEDS_ACTION); + break; + case Attendees.ATTENDEE_STATUS_ACCEPTED: + params.add(PartStat.ACCEPTED); + break; + case Attendees.ATTENDEE_STATUS_DECLINED: + params.add(PartStat.DECLINED); + break; + case Attendees.ATTENDEE_STATUS_TENTATIVE: + params.add(PartStat.TENTATIVE); + break; + } + + e.addAttendee(attendee); + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't parse attendee information, ignoring", ex); + } + } + } + + void populateReminders(Event e) throws RemoteException { + // reminders + Uri remindersUri = Reminders.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + @Cleanup Cursor c = providerClient.query(remindersUri, new String[] { + /* 0 */ Reminders.MINUTES, Reminders.METHOD + }, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + while (c != null && c.moveToNext()) { + VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0)); + + PropertyList props = alarm.getProperties(); + switch (c.getInt(1)) { + /*case Reminders.METHOD_EMAIL: + props.add(Action.EMAIL); + break;*/ + default: + props.add(Action.DISPLAY); + props.add(new Description(e.getSummary())); + } + e.addAlarm(alarm); + } + } + + + /* content builder methods */ + + @Override + protected Builder buildEntry(Builder builder, Resource resource) { + Event event = (Event)resource; + + builder = builder + .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.EVENT_TIMEZONE, event.getDtStartTzID()) + .withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1) + .withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1) + .withValue(Events.GUESTS_CAN_MODIFY, 1) + .withValue(Events.GUESTS_CAN_SEE_GUESTS, 1); + + boolean recurring = false; + if (event.getRrule() != null) { + recurring = true; + builder = builder.withValue(Events.RRULE, event.getRrule().getValue()); + } + if (event.getRdate() != null) { + recurring = true; + builder = builder.withValue(Events.RDATE, event.getRdate().getValue()); + } + if (event.getExrule() != null) + builder = builder.withValue(Events.EXRULE, event.getExrule().getValue()); + if (event.getExdate() != null) + builder = builder.withValue(Events.EXDATE, event.getExdate().getValue()); + + // set either DTEND for single-time events or DURATION for recurring events + // because that's the way Android likes it (see docs) + if (recurring) { + // calculate DURATION from start and end date + Duration duration = new Duration(event.getDtStart().getDate(), event.getDtEnd().getDate()); + builder = builder.withValue(Events.DURATION, duration.getValue()); + } else { + builder = builder + .withValue(Events.DTEND, event.getDtEndInMillis()) + .withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID()); + } + + if (event.getSummary() != null) + builder = builder.withValue(Events.TITLE, event.getSummary()); + if (event.getLocation() != null) + builder = builder.withValue(Events.EVENT_LOCATION, event.getLocation()); + if (event.getDescription() != null) + builder = builder.withValue(Events.DESCRIPTION, event.getDescription()); + + if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) { + URI organizer = event.getOrganizer().getCalAddress(); + if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto")) + builder = builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart()); + } + + Status status = event.getStatus(); + if (status != null) { + int statusCode = Events.STATUS_TENTATIVE; + if (status == Status.VEVENT_CONFIRMED) + statusCode = Events.STATUS_CONFIRMED; + else if (status == Status.VEVENT_CANCELLED) + statusCode = Events.STATUS_CANCELED; + builder = builder.withValue(Events.STATUS, statusCode); + } + + builder = builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE); + + if (event.getForPublic() != null) + builder = builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE); + + return builder; + } + + + @Override + protected void addDataRows(Resource resource, long localID, int backrefIdx) { + Event event = (Event)resource; + for (Attendee attendee : event.getAttendees()) + pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build()); + for (VAlarm alarm : event.getAlarms()) + pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build()); + } + + @Override + protected void removeDataRows(Resource resource) { + Event event = (Event)resource; + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI)) + .withSelection(Attendees.EVENT_ID + "=?", + new String[] { String.valueOf(event.getLocalID()) }).build()); + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI)) + .withSelection(Reminders.EVENT_ID + "=?", + new String[] { String.valueOf(event.getLocalID()) }).build()); + } + + + @SuppressLint("InlinedApi") + protected Builder buildAttendee(Builder builder, Attendee attendee) { + Uri member = Uri.parse(attendee.getValue()); + String email = member.getSchemeSpecificPart(); + + Cn cn = (Cn)attendee.getParameter(Parameter.CN); + if (cn != null) + builder = builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue()); + + int type = Attendees.TYPE_NONE; + + CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE); + if (cutype == CuType.RESOURCE) + type = Attendees.TYPE_RESOURCE; + else { + Role role = (Role)attendee.getParameter(Parameter.ROLE); + int relationship; + if (role == Role.CHAIR) + relationship = Attendees.RELATIONSHIP_ORGANIZER; + else { + relationship = Attendees.RELATIONSHIP_ATTENDEE; + if (role == Role.OPT_PARTICIPANT) + type = Attendees.TYPE_OPTIONAL; + else if (role == Role.REQ_PARTICIPANT) + type = Attendees.TYPE_REQUIRED; + } + builder = builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship); + } + + int status = Attendees.ATTENDEE_STATUS_NONE; + PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT); + if (partStat == null || partStat == PartStat.NEEDS_ACTION) + status = Attendees.ATTENDEE_STATUS_INVITED; + else if (partStat == PartStat.ACCEPTED) + status = Attendees.ATTENDEE_STATUS_ACCEPTED; + else if (partStat == PartStat.DECLINED) + status = Attendees.ATTENDEE_STATUS_DECLINED; + else if (partStat == PartStat.TENTATIVE) + status = Attendees.ATTENDEE_STATUS_TENTATIVE; + + return builder + .withValue(Attendees.ATTENDEE_EMAIL, email) + .withValue(Attendees.ATTENDEE_TYPE, type) + .withValue(Attendees.ATTENDEE_STATUS, status); + } + + protected Builder buildReminder(Builder builder, VAlarm alarm) { + int minutes = 0; + + Dur duration; + if (alarm.getTrigger() != null && (duration = alarm.getTrigger().getDuration()) != null) + minutes = duration.getDays() * 24*60 + duration.getHours()*60 + duration.getMinutes(); + + Log.d(TAG, "Adding alarm " + minutes + " min before"); + + return builder + .withValue(Reminders.METHOD, Reminders.METHOD_ALERT) + .withValue(Reminders.MINUTES, minutes); + } + + + + /* private helper methods */ + + protected static Uri calendarsURI(Account account) { + return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build(); + } + + protected Uri calendarsURI() { + return calendarsURI(account); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java new file mode 100644 index 00000000..24293421 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java @@ -0,0 +1,361 @@ +/******************************************************************************* + * 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; + +import java.util.ArrayList; + +import lombok.Cleanup; +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.util.Log; + +/** + * Represents a locally-stored synchronizable collection (for instance, the + * address book or a calendar). Manages a CTag that stores the last known + * remote CTag (the remote CTag changes whenever something in the remote collection changes). + * + * @param Subtype of Resource that can be stored in the collection + */ +public abstract class LocalCollection { + private static final String TAG = "davdroid.LocalCollection"; + + protected Account account; + protected ContentProviderClient providerClient; + protected ArrayList pendingOperations = new ArrayList(); + + + // database fields + + /** base Uri of the collection's entries (for instance, Events.CONTENT_URI); + * apply syncAdapterURI() before returning a value */ + abstract protected Uri entriesURI(); + + /** column name of the type of the account the entry belongs to */ + abstract protected String entryColumnAccountType(); + /** column name of the name of the account the entry belongs to */ + abstract protected String entryColumnAccountName(); + + /** column name of the collection ID the entry belongs to */ + abstract protected String entryColumnParentID(); + /** column name of an entry's ID */ + abstract protected String entryColumnID(); + /** column name of an entry's file name on the WebDAV server */ + abstract protected String entryColumnRemoteName(); + /** column name of an entry's last ETag on the WebDAV server; null if entry hasn't been uploaded yet */ + abstract protected String entryColumnETag(); + + /** column name of an entry's "dirty" flag (managed by content provider) */ + abstract protected String entryColumnDirty(); + /** column name of an entry's "deleted" flag (managed by content provider) */ + abstract protected String entryColumnDeleted(); + + /** column name of an entry's UID */ + abstract protected String entryColumnUID(); + + + LocalCollection(Account account, ContentProviderClient providerClient) { + this.account = account; + this.providerClient = providerClient; + } + + + // collection operations + + /** gets the ID if the collection (for instance, ID of the Android calendar) */ + abstract public long getId(); + /** gets the CTag of the collection */ + abstract public String getCTag() throws LocalStorageException; + /** sets the CTag of the collection */ + abstract public void setCTag(String cTag) throws LocalStorageException; + + + // content provider (= database) querying + + /** + * Finds new resources (resources which haven't been uploaded yet). + * New resources are 1) dirty, and 2) don't have an ETag yet. + * + * @return IDs of new resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findNew() throws LocalStorageException { + String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query new records"); + + long[] fresh = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) { + long id = cursor.getLong(0); + + // new record: generate UID + remote file name so that we can upload + T resource = findById(id, false); + resource.initialize(); + // write generated UID + remote file name into database + ContentValues values = new ContentValues(2); + values.put(entryColumnUID(), resource.getUid()); + values.put(entryColumnRemoteName(), resource.getName()); + providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null); + + fresh[idx] = id; + } + return fresh; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds updated resources (resources which have already been uploaded, but have changed locally). + * Updated resources are 1) dirty, and 2) already have an ETag. + * + * @return IDs of updated resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findUpdated() throws LocalStorageException { + String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query updated records"); + + long[] updated = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) + updated[idx] = cursor.getLong(0); + return updated; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds deleted resources (resources which have been marked for deletion). + * Deleted resources have the "deleted" flag set. + * + * @return IDs of deleted resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findDeleted() throws LocalStorageException { + String where = entryColumnDeleted() + "=1"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query dirty records"); + + long deleted[] = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) + deleted[idx] = cursor.getLong(0); + return deleted; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds a specific resource by ID. + * @param localID ID of the resource + * @param populate true: populates all data fields (for instance, contact or event details); + * false: only remote file name and ETag are populated + * @return resource with either ID/remote file/name/ETag or all fields populated + * @throws RecordNotFoundException when the resource couldn't be found + * @throws LocalStorageException when the content provider couldn't be queried + */ + public T findById(long localID, boolean populate) throws LocalStorageException { + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID), + new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(localID, cursor.getString(0), cursor.getString(1)); + if (populate) + populate(resource); + return resource; + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds a specific resource by remote file name. + * @param localID remote file name of the resource + * @param populate true: populates all data fields (for instance, contact or event details); + * false: only remote file name and ETag are populated + * @return resource with either ID/remote file/name/ETag or all fields populated + * @throws RecordNotFoundException when the resource couldn't be found + * @throws LocalStorageException when the content provider couldn't be queried + */ + public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException { + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + entryColumnRemoteName() + "=?", new String[] { remoteName }, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2)); + if (populate) + populate(resource); + return resource; + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** populates all data fields from the content provider */ + public abstract void populate(Resource record) throws LocalStorageException; + + + // create/update/delete + + /** + * Creates a new resource object in memory. No content provider operations involved. + * @param localID the ID of the resource + * @param resourceName the (remote) file name of the resource + * @param ETag of the resource + * @return the new resource object */ + abstract public T newResource(long localID, String resourceName, String eTag); + + /** Enqueues adding the resource (including all data) to the local collection. Requires commit(). */ + public void add(Resource resource) { + int idx = pendingOperations.size(); + pendingOperations.add( + buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource) + .withYieldAllowed(true) + .build()); + + addDataRows(resource, -1, idx); + } + + /** Enqueues updating an existing resource in the local collection. The resource will be found by + * the remote file name and all data will be updated. Requires commit(). */ + public void updateByRemoteName(Resource remoteResource) throws LocalStorageException { + T localResource = findByRemoteName(remoteResource.getName(), false); + pendingOperations.add( + buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource) + .withValue(entryColumnETag(), remoteResource.getETag()) + .withYieldAllowed(true) + .build()); + + removeDataRows(localResource); + addDataRows(remoteResource, localResource.getLocalID(), -1); + } + + /** Enqueues deleting a resource from the local collection. Requires commit(). */ + public void delete(Resource resource) { + pendingOperations.add(ContentProviderOperation + .newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID())) + .withYieldAllowed(true) + .build()); + } + + /** + * Enqueues deleting all resources except the give ones from the local collection. Requires commit(). + * @param remoteResources resources with these remote file names will be kept + */ + public abstract void deleteAllExceptRemoteNames(Resource[] remoteResources); + + /** Updates the locally-known ETag of a resource. */ + public void updateETag(Resource res, String eTag) throws LocalStorageException { + Log.d(TAG, "Setting ETag of local resource " + res + " to " + eTag); + + ContentValues values = new ContentValues(1); + values.put(entryColumnETag(), eTag); + try { + providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {}); + } catch (RemoteException e) { + throw new LocalStorageException(e); + } + } + + /** Enqueues removing the dirty flag from a locally-stored resource. Requires commit(). */ + public void clearDirty(Resource resource) { + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID())) + .withValue(entryColumnDirty(), 0) + .build()); + } + + /** Commits enqueued operations to the content provider (for batch operations). */ + public void commit() throws LocalStorageException { + if (!pendingOperations.isEmpty()) + try { + Log.d(TAG, "Committing " + pendingOperations.size() + " operations"); + providerClient.applyBatch(pendingOperations); + pendingOperations.clear(); + } catch (RemoteException ex) { + throw new LocalStorageException(ex); + } catch(OperationApplicationException ex) { + throw new LocalStorageException(ex); + } + } + + + // helpers + + protected void queueOperation(Builder builder) { + if (builder != null) + pendingOperations.add(builder.build()); + } + + /** Appends account type, name and CALLER_IS_SYNCADAPTER to an Uri. */ + protected Uri syncAdapterURI(Uri baseURI) { + return baseURI.buildUpon() + .appendQueryParameter(entryColumnAccountType(), account.type) + .appendQueryParameter(entryColumnAccountName(), account.name) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + } + + protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) { + Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri)); + if (backrefIdx != -1) + return builder.withValueBackReference(refFieldName, backrefIdx); + else + return builder.withValue(refFieldName, raw_ref_id); + } + + + // content builders + + /** + * Builds the main entry (for instance, a ContactsContract.RawContacts row) from a resource. + * The entry is built for insertion to the location identified by entriesURI(). + * + * @param builder Builder to be extended by all resource data that can be stored without extra data rows. + */ + protected abstract Builder buildEntry(Builder builder, Resource resource); + + /** Enqueues adding extra data rows of the resource to the local collection. */ + protected abstract void addDataRows(Resource resource, long localID, int backrefIdx); + + /** Enqueues removing all extra data rows of the resource from the local collection. */ + protected abstract void removeDataRows(Resource resource); +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java new file mode 100644 index 00000000..d33ad46a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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; + +public class LocalStorageException extends Exception { + private static final long serialVersionUID = -7787658815291629529L; + + private static final String detailMessage = "Couldn't access local content provider"; + + + public LocalStorageException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public LocalStorageException(String detailMessage) { + super(detailMessage); + } + + public LocalStorageException(Throwable throwable) { + super(detailMessage, throwable); + } + + public LocalStorageException() { + super(detailMessage); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java b/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java new file mode 100644 index 00000000..98daa77c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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; + +/** + * Thrown when a local record (for instance, Contact with ID 12345) should be read + * but could not be found. + */ +public class RecordNotFoundException extends LocalStorageException { + private static final long serialVersionUID = 4961024282198632578L; + + private static final String detailMessage = "Record not found in local content provider"; + + + RecordNotFoundException(Throwable ex) { + super(detailMessage, ex); + } + + RecordNotFoundException() { + super(detailMessage); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java new file mode 100644 index 00000000..5d733e15 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import lombok.Cleanup; +import lombok.Getter; +import net.fortuna.ical4j.model.ValidationException; +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavMultiget; +import at.bitfire.davdroid.webdav.DavNoContentException; +import at.bitfire.davdroid.webdav.HttpException; +import at.bitfire.davdroid.webdav.HttpPropfind; +import at.bitfire.davdroid.webdav.WebDavResource; +import at.bitfire.davdroid.webdav.WebDavResource.PutMode; +import org.apache.http.impl.client.CloseableHttpClient; +import ezvcard.io.text.VCardParseException; + +/** + * Represents a remotely stored synchronizable collection (collection as in + * WebDAV terminology). + * + * @param Subtype of Resource that can be stored in the collection + */ +public abstract class RemoteCollection { + private static final String TAG = "davdroid.RemoteCollection"; + + CloseableHttpClient httpClient; + @Getter WebDavResource collection; + + abstract protected String memberContentType(); + abstract protected DavMultiget.Type multiGetType(); + abstract protected T newResourceSkeleton(String name, String ETag); + + public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + this.httpClient = httpClient; + + collection = new WebDavResource(httpClient, URIUtils.parseURI(baseURL), user, password, preemptiveAuth); + } + + + /* collection operations */ + + public String getCTag() throws URISyntaxException, IOException, HttpException { + try { + if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched + collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG); + } catch (DavException e) { + return null; + } + return collection.getCTag(); + } + + public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException { + collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG); + + List resources = new LinkedList(); + if (collection.getMembers() != null) { + for (WebDavResource member : collection.getMembers()) + resources.add(newResourceSkeleton(member.getName(), member.getETag())); + } + return resources.toArray(new Resource[0]); + } + + @SuppressWarnings("unchecked") + public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException { + try { + if (resources.length == 1) + return (T[]) new Resource[] { get(resources[0]) }; + + Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)"); + + LinkedList names = new LinkedList(); + for (Resource resource : resources) + names.add(resource.getName()); + + LinkedList foundResources = new LinkedList(); + collection.multiGet(multiGetType(), names.toArray(new String[0])); + if (collection.getMembers() == null) + throw new DavNoContentException(); + + for (WebDavResource member : collection.getMembers()) { + T resource = newResourceSkeleton(member.getName(), member.getETag()); + try { + if (member.getContent() != null) { + @Cleanup InputStream is = new ByteArrayInputStream(member.getContent()); + resource.parseEntity(is); + foundResources.add(resource); + } else + Log.e(TAG, "Ignoring entity without content"); + } catch (InvalidResourceException e) { + Log.e(TAG, "Ignoring unparseable entity in multi-response", e); + } + } + + return foundResources.toArray(new Resource[0]); + } catch (InvalidResourceException e) { + Log.e(TAG, "Couldn't parse entity from GET", e); + } + + return new Resource[0]; + } + + + /* internal member operations */ + + public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException { + WebDavResource member = new WebDavResource(collection, resource.getName()); + + if (resource instanceof Contact) + member.get(Contact.MIME_TYPE); + else if (resource instanceof Event) + member.get(Event.MIME_TYPE); + else { + Log.wtf(TAG, "Should fetch something, but neither contact nor calendar"); + throw new InvalidResourceException("Didn't now which MIME type to accept"); + } + + byte[] data = member.getContent(); + if (data == null) + throw new DavNoContentException(); + + @Cleanup InputStream is = new ByteArrayInputStream(data); + try { + resource.parseEntity(is); + } catch(VCardParseException e) { + throw new InvalidResourceException(e); + } + return resource; + } + + // returns ETag of the created resource, if returned by server + public String add(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.setContentType(memberContentType()); + + @Cleanup ByteArrayOutputStream os = res.toEntity(); + String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE); + + // after a successful upload, the collection has implicitely changed, too + collection.invalidateCTag(); + + return eTag; + } + + public void delete(Resource res) throws URISyntaxException, IOException, HttpException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.delete(); + + collection.invalidateCTag(); + } + + // returns ETag of the updated resource, if returned by server + public String update(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.setContentType(memberContentType()); + + @Cleanup ByteArrayOutputStream os = res.toEntity(); + String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE); + + // after a successful upload, the collection has implicitely changed, too + collection.invalidateCTag(); + + return eTag; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java new file mode 100644 index 00000000..bda3e2e5 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a resource that can be contained in a LocalCollection or RemoteCollection + * for synchronization by WebDAV. + */ +@ToString +public abstract class Resource { + @Getter @Setter protected String name, ETag; + @Getter @Setter protected String uid; + @Getter protected long localID; + + + public Resource(String name, String ETag) { + this.name = name; + this.ETag = ETag; + } + + public Resource(long localID, String name, String ETag) { + this(name, ETag); + this.localID = localID; + } + + /** initializes UID and remote file name (required for first upload) */ + public abstract void initialize(); + + /** fills the resource data from an input stream (for instance, .vcf file for Contact) */ + public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException; + /** writes the resource data to an output stream (for instance, .vcf file for Contact) */ + public abstract ByteArrayOutputStream toEntity() throws IOException; +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java new file mode 100644 index 00000000..14f5b6f7 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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; + +import java.io.Serializable; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; + +import ezvcard.VCardVersion; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(suppressConstructorProperties=true) +@Data +public class ServerInfo implements Serializable { + private static final long serialVersionUID = 6744847358282980437L; + + enum Scheme { + HTTP, HTTPS, MAILTO + } + + final private URI baseURI; + final private String userName, password; + final boolean authPreemptive; + + private String errorMessage; + + private boolean calDAV = false, cardDAV = false; + private List + addressBooks = new LinkedList(), + calendars = new LinkedList(); + + + public boolean hasEnabledCalendars() { + for (ResourceInfo calendar : calendars) + if (calendar.enabled) + return true; + return false; + } + + + @RequiredArgsConstructor(suppressConstructorProperties=true) + @Data + public static class ResourceInfo implements Serializable { + private static final long serialVersionUID = -5516934508229552112L; + + public enum Type { + ADDRESS_BOOK, + CALENDAR + } + + boolean enabled = false; + + final Type type; + final boolean readOnly; + final String URL, title, description, color; + + VCardVersion vCardVersion; + + String timezone; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java new file mode 100644 index 00000000..f78cc4f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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.syncadapter; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class AccountAuthenticatorService extends Service { + private static AccountAuthenticator accountAuthenticator; + + private AccountAuthenticator getAuthenticator() { + if (accountAuthenticator != null) + return accountAuthenticator; + return accountAuthenticator = new AccountAuthenticator(this); + } + + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) + return getAuthenticator().getIBinder(); + return null; + } + + + private static class AccountAuthenticator extends AbstractAccountAuthenticator { + Context context; + + public AccountAuthenticator(Context context) { + super(context); + this.context = context; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, + String[] requiredFeatures, Bundle options) throws NetworkErrorException { + Intent intent = new Intent(context, AddAccountActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java new file mode 100644 index 00000000..37ab4734 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * 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.syncadapter; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Fragment; +import android.content.ContentResolver; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.provider.ContactsContract; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.ServerInfo; + +public class AccountDetailsFragment extends Fragment implements TextWatcher { + public static final String KEY_SERVER_INFO = "server_info"; + + ServerInfo serverInfo; + + EditText editAccountName; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.account_details, container, false); + + serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + + editAccountName = (EditText)v.findViewById(R.id.account_name); + editAccountName.addTextChangedListener(this); + editAccountName.setText(serverInfo.getUserName()); + + TextView textAccountNameInfo = (TextView)v.findViewById(R.id.account_name_info); + if (!serverInfo.hasEnabledCalendars()) + textAccountNameInfo.setVisibility(View.GONE); + + setHasOptionsMenu(true); + return v; + } + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.account_details, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_account: + addAccount(); + break; + default: + return false; + } + return true; + } + + + // actions + + void addAccount() { + ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + String accountName = editAccountName.getText().toString(); + + AccountManager accountManager = AccountManager.get(getActivity()); + Account account = new Account(accountName, Constants.ACCOUNT_TYPE); + Bundle userData = AccountSettings.createBundle(serverInfo); + + boolean syncContacts = false; + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + if (addressBook.isEnabled()) { + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); + syncContacts = true; + continue; + } + if (syncContacts) { + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); + } else + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0); + + if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) { + // account created, now create calendars + boolean syncCalendars = false; + for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars()) + if (calendar.isEnabled()) + try { + LocalCalendar.create(account, getActivity().getContentResolver(), calendar); + syncCalendars = true; + } catch (LocalStorageException e) { + Toast.makeText(getActivity(), "Couldn't create calendar(s): " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + if (syncCalendars) { + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true); + } else + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0); + + getActivity().finish(); + } else + Toast.makeText(getActivity(), "Couldn't create account (account with this name already existing?)", Toast.LENGTH_LONG).show(); + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean ok = false; + ok = editAccountName.getText().length() > 0; + MenuItem item = menu.findItem(R.id.add_account); + item.setEnabled(ok); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java new file mode 100644 index 00000000..a58fe407 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import lombok.Cleanup; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.provider.CalendarContract.Calendars; +import android.util.Log; +import at.bitfire.davdroid.resource.ServerInfo; +import ezvcard.VCardVersion; + +public class AccountSettings { + private final static String TAG = "davdroid.AccountSettings"; + + private final static int CURRENT_VERSION = 1; + private final static String + KEY_SETTINGS_VERSION = "version", + + KEY_USERNAME = "user_name", + KEY_AUTH_PREEMPTIVE = "auth_preemptive", + + KEY_ADDRESSBOOK_URL = "addressbook_url", + KEY_ADDRESSBOOK_CTAG = "addressbook_ctag", + KEY_ADDRESSBOOK_VCARD_VERSION = "addressbook_vcard_version"; + + Context context; + AccountManager accountManager; + Account account; + + + public AccountSettings(Context context, Account account) { + this.context = context; + this.account = account; + + accountManager = AccountManager.get(context); + + synchronized(AccountSettings.class) { + int version = 0; + try { + version = Integer.parseInt(accountManager.getUserData(account, KEY_SETTINGS_VERSION)); + } catch(NumberFormatException e) { + } + if (version < CURRENT_VERSION) + update(version); + } + } + + + public static Bundle createBundle(ServerInfo serverInfo) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION)); + bundle.putString(KEY_USERNAME, serverInfo.getUserName()); + bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive())); + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + if (addressBook.isEnabled()) { + bundle.putString(KEY_ADDRESSBOOK_URL, addressBook.getURL()); + bundle.putString(KEY_ADDRESSBOOK_VCARD_VERSION, addressBook.getVCardVersion().getVersion()); + continue; + } + return bundle; + } + + + // general settings + + public String getUserName() { + return accountManager.getUserData(account, KEY_USERNAME); + } + + public String getPassword() { + return accountManager.getPassword(account); + } + + public boolean getPreemptiveAuth() { + return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); + } + + + // address book (CardDAV) settings + + public String getAddressBookURL() { + return accountManager.getUserData(account, KEY_ADDRESSBOOK_URL); + } + + public String getAddressBookCTag() { + return accountManager.getUserData(account, KEY_ADDRESSBOOK_CTAG); + } + + public void setAddressBookCTag(String cTag) { + accountManager.setUserData(account, KEY_ADDRESSBOOK_CTAG, cTag); + } + + public VCardVersion getAddressBookVCardVersion() { + VCardVersion version = VCardVersion.V3_0; + String versionStr = accountManager.getUserData(account, KEY_ADDRESSBOOK_VCARD_VERSION); + if (versionStr != null) + version = VCardVersion.valueOfByStr(versionStr); + return version; + } + + + // update from previous account settings + + private void update(int fromVersion) { + Log.i(TAG, "Account settings must be updated from v" + fromVersion + " to v" + CURRENT_VERSION); + for (int toVersion = CURRENT_VERSION; toVersion > fromVersion; toVersion--) + update(fromVersion, toVersion); + } + + private void update(int fromVersion, int toVersion) { + Log.i(TAG, "Updating account settings from v" + fromVersion + " to " + toVersion); + try { + if (fromVersion == 0 && toVersion == 1) + update_0_1(); + else + Log.wtf(TAG, "Don't know how to update settings from v" + fromVersion + " to v" + toVersion); + } catch(Exception e) { + Log.e(TAG, "Couldn't update account settings (DAVdroid will probably crash)!", e); + } + } + + private void update_0_1() throws URISyntaxException { + String v0_principalURL = accountManager.getUserData(account, "principal_url"), + v0_addressBookPath = accountManager.getUserData(account, "addressbook_path"); + Log.d(TAG, "Old principal URL = " + v0_principalURL); + Log.d(TAG, "Old address book path = " + v0_addressBookPath); + + URI principalURI = new URI(v0_principalURL); + + // update address book + if (v0_addressBookPath != null) { + String addressBookURL = principalURI.resolve(v0_addressBookPath).toASCIIString(); + Log.d(TAG, "New address book URL = " + addressBookURL); + accountManager.setUserData(account, "addressbook_url", addressBookURL); + } + + // update calendars + ContentResolver resolver = context.getContentResolver(); + Uri calendars = Calendars.CONTENT_URI.buildUpon() + .appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build(); + @Cleanup Cursor cursor = resolver.query(calendars, new String[] { Calendars._ID, Calendars.NAME }, null, null, null); + while (cursor != null && cursor.moveToNext()) { + int id = cursor.getInt(0); + String v0_path = cursor.getString(1), + v1_url = principalURI.resolve(v0_path).toASCIIString(); + Log.d(TAG, "Updating calendar #" + id + " name: " + v0_path + " -> " + v1_url); + Uri calendar = ContentUris.appendId(Calendars.CONTENT_URI.buildUpon() + .appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true"), id).build(); + ContentValues newValues = new ContentValues(1); + newValues.put(Calendars.NAME, v1_url); + if (resolver.update(calendar, newValues, null, null) != 1) + Log.e(TAG, "Number of modified calendars != 1"); + } + + Log.d(TAG, "Cleaning old principal URL and address book path"); + accountManager.setUserData(account, "principal_url", null); + accountManager.setUserData(account, "addressbook_path", null); + + Log.d(TAG, "Updated settings successfully!"); + accountManager.setUserData(account, KEY_SETTINGS_VERSION, "1"); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java new file mode 100644 index 00000000..f65f42f9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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.syncadapter; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.R; + +public class AddAccountActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.add_account); + + if (savedInstanceState == null) { // first call + getFragmentManager().beginTransaction() + .add(R.id.fragment_container, new LoginTypeFragment(), "login_type") + .commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.add_account, menu); + return true; + } + + public void showHelp(MenuItem item) { + startActivityForResult(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_HELP)), 0); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java new file mode 100644 index 00000000..2746163b --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import at.bitfire.davdroid.resource.CalDavCalendar; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.RemoteCollection; + +public class CalendarsSyncAdapterService extends Service { + private static SyncAdapter syncAdapter; + + + @Override + public void onCreate() { + if (syncAdapter == null) + syncAdapter = new SyncAdapter(getApplicationContext()); + } + + @Override + public void onDestroy() { + syncAdapter.close(); + syncAdapter = null; + } + + @Override + public IBinder onBind(Intent intent) { + return syncAdapter.getSyncAdapterBinder(); + } + + + private static class SyncAdapter extends DavSyncAdapter { + private final static String TAG = "davdroid.CalendarsSyncAdapter"; + + + private SyncAdapter(Context context) { + super(context); + } + + @Override + protected Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) { + AccountSettings settings = new AccountSettings(getContext(), account); + String userName = settings.getUserName(), + password = settings.getPassword(); + boolean preemptive = settings.getPreemptiveAuth(); + + try { + Map, RemoteCollection> map = new HashMap, RemoteCollection>(); + + for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) { + RemoteCollection dav = new CalDavCalendar(httpClient, calendar.getUrl(), userName, password, preemptive); + map.put(calendar, dav); + } + return map; + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't find local calendars", ex); + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't build calendar URI", ex); + } + + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java new file mode 100644 index 00000000..7e8f02a2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java @@ -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.syncadapter; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import at.bitfire.davdroid.resource.CardDavAddressBook; +import at.bitfire.davdroid.resource.LocalAddressBook; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.RemoteCollection; + +public class ContactsSyncAdapterService extends Service { + private static ContactsSyncAdapter syncAdapter; + + + @Override + public void onCreate() { + if (syncAdapter == null) + syncAdapter = new ContactsSyncAdapter(getApplicationContext()); + } + + @Override + public void onDestroy() { + syncAdapter.close(); + syncAdapter = null; + } + + @Override + public IBinder onBind(Intent intent) { + return syncAdapter.getSyncAdapterBinder(); + } + + + private static class ContactsSyncAdapter extends DavSyncAdapter { + private final static String TAG = "davdroid.ContactsSyncAdapter"; + + + private ContactsSyncAdapter(Context context) { + super(context); + } + + @Override + protected Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) { + AccountSettings settings = new AccountSettings(getContext(), account); + String userName = settings.getUserName(), + password = settings.getPassword(); + boolean preemptive = settings.getPreemptiveAuth(); + + String addressBookURL = settings.getAddressBookURL(); + if (addressBookURL == null) + return null; + + try { + LocalCollection database = new LocalAddressBook(account, provider, settings); + RemoteCollection dav = new CardDavAddressBook(httpClient, addressBookURL, userName, password, preemptive); + + Map, RemoteCollection> map = new HashMap, RemoteCollection>(); + map.put(database, dav); + + return map; + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't build address book URI", ex); + } + + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java new file mode 100644 index 00000000..00d045af --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import lombok.Getter; + +import org.apache.http.HttpStatus; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.util.Log; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.RemoteCollection; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavHttpClient; +import at.bitfire.davdroid.webdav.HttpException; +import org.apache.http.impl.client.CloseableHttpClient; + +public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable { + private final static String TAG = "davdroid.DavSyncAdapter"; + + @Getter private static String androidID; + + protected AccountManager accountManager; + + /* We use one static httpClient for + * - all sync adapters (CalendarsSyncAdapter, ContactsSyncAdapter) + * - and all threads (= accounts) of each sync adapter + * so that HttpClient's threaded pool management can do its best. + */ + protected static CloseableHttpClient httpClient; + + /* One static read/write lock pair for the static httpClient: + * Use the READ lock when httpClient will only be called (to prevent it from being unset while being used). + * Use the WRITE lock when httpClient will be modified (set/unset). */ + private final static ReentrantReadWriteLock httpClientLock = new ReentrantReadWriteLock(); + + + public DavSyncAdapter(Context context) { + super(context, true); + + synchronized(this) { + if (androidID == null) + androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + accountManager = AccountManager.get(context); + } + + @Override + public void close() { + Log.d(TAG, "Closing httpClient"); + + // may be called from a GUI thread, so we need an AsyncTask + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + try { + httpClientLock.writeLock().lock(); + if (httpClient != null) { + httpClient.close(); + httpClient = null; + } + httpClientLock.writeLock().unlock(); + } catch (IOException e) { + Log.w(TAG, "Couldn't close HTTP client", e); + } + return null; + } + }.execute(); + } + + protected abstract Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider); + + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + Log.i(TAG, "Performing sync for authority " + authority); + + // set class loader for iCal4j ResourceLoader + Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); + + // create httpClient, if necessary + httpClientLock.writeLock().lock(); + if (httpClient == null) { + Log.d(TAG, "Creating new DavHttpClient"); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getContext()); + httpClient = DavHttpClient.create( + settings.getBoolean(Constants.SETTING_DISABLE_COMPRESSION, false), + settings.getBoolean(Constants.SETTING_NETWORK_LOGGING, false) + ); + } + + // prevent httpClient shutdown until we're ready by holding a read lock + // acquiring read lock before releasing write lock will downgrade the write lock to a read lock + httpClientLock.readLock().lock(); + httpClientLock.writeLock().unlock(); + + // TODO use VCard 4.0 if possible + AccountSettings accountSettings = new AccountSettings(getContext(), account); + Log.d(TAG, "Server supports VCard version " + accountSettings.getAddressBookVCardVersion()); + + try { + // get local <-> remote collection pairs + Map, RemoteCollection> syncCollections = getSyncPairs(account, provider); + if (syncCollections == null) + Log.i(TAG, "Nothing to synchronize"); + else + try { + for (Map.Entry, RemoteCollection> entry : syncCollections.entrySet()) + new SyncManager(entry.getKey(), entry.getValue()).synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult); + } catch (DavException ex) { + syncResult.stats.numParseExceptions++; + Log.e(TAG, "Invalid DAV response", ex); + } catch (HttpException ex) { + if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) { + Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex); + syncResult.stats.numAuthExceptions++; + } else if (ex.isClientError()) { + Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex); + syncResult.stats.numParseExceptions++; + } else { + Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex); + syncResult.stats.numIoExceptions++; + } + } catch (LocalStorageException ex) { + syncResult.databaseError = true; + Log.e(TAG, "Local storage (content provider) exception", ex); + } catch (IOException ex) { + syncResult.stats.numIoExceptions++; + Log.e(TAG, "I/O error (Android will try again later)", ex); + } catch (URISyntaxException ex) { + Log.e(TAG, "Invalid URI (file name) syntax", ex); + } + } finally { + // allow httpClient shutdown + httpClientLock.readLock().unlock(); + } + + Log.i(TAG, "Sync complete for " + authority); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java new file mode 100644 index 00000000..9135b428 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * 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.syncadapter; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import at.bitfire.davdroid.R; + +public class GeneralSettingsActivity extends Activity { + final static String URL_REPORT_ISSUE = "https://github.com/bitfireAT/davdroid/blob/master/CONTRIBUTING.md#reporting-issues"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new GeneralSettingsFragment()) + .commit(); + } + + public void reportIssue(MenuItem item) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URL_REPORT_ISSUE))); + } + + + public static class GeneralSettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getPreferenceManager().setSharedPreferencesMode(Context.MODE_MULTI_PROCESS); + addPreferencesFromResource(R.xml.general_settings); + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.debug_settings, menu); + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java new file mode 100644 index 00000000..f0879941 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import at.bitfire.davdroid.R; + +public class LoginEmailFragment extends Fragment implements TextWatcher { + + protected EditText editEmail, editPassword; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_email, container, false); + + editEmail = (EditText)v.findViewById(R.id.email_address); + editEmail.addTextChangedListener(this); + editPassword = (EditText)v.findViewById(R.id.password); + editPassword.addTextChangedListener(this); + + setHasOptionsMenu(true); + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + FragmentTransaction ft = getFragmentManager().beginTransaction(); + + Bundle args = new Bundle(); + String email = editEmail.getText().toString(); + args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, "mailto:" + email); + args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, email); + args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString()); + args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, true); + + DialogFragment dialog = new QueryServerDialogFragment(); + dialog.setArguments(args); + dialog.show(ft, QueryServerDialogFragment.class.getName()); + break; + default: + return false; + } + return true; + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean emailOk = false, + passwordOk = editPassword.getText().length() > 0; + + String email = editEmail.getText().toString(); + try { + URI uri = new URI("mailto:" + email); + if (uri.isOpaque()) { + int pos = email.lastIndexOf("@"); + if (pos != -1) + emailOk = !email.substring(pos+1).isEmpty(); + } + } catch (URISyntaxException e) { + // invalid mailto: URI + } + + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(emailOk && passwordOk); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java new file mode 100644 index 00000000..8507bcc6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.syncadapter; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import at.bitfire.davdroid.R; + +public class LoginTypeFragment extends Fragment { + + protected RadioButton btnTypeEmail, btnTypeURL; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_type, container, false); + + btnTypeEmail = (RadioButton)v.findViewById(R.id.login_type_email); + btnTypeURL = (RadioButton)v.findViewById(R.id.login_type_url); + + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + Fragment loginFragment = btnTypeEmail.isChecked() ? new LoginEmailFragment() : new LoginURLFragment(); + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, loginFragment) + .addToBackStack(null) + .commitAllowingStateLoss(); + return true; + default: + return false; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java new file mode 100644 index 00000000..5703fa93 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.commons.lang.StringUtils; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +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; +import at.bitfire.davdroid.R; + +public class LoginURLFragment extends Fragment implements TextWatcher { + + protected Spinner spnrScheme; + protected TextView textHttpWarning; + protected EditText editBaseURI, editUserName, editPassword; + protected CheckBox checkboxPreemptive; + protected Button btnNext; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_url, container, false); + + // protocol selection spinner + textHttpWarning = (TextView)v.findViewById(R.id.http_warning); + + spnrScheme = (Spinner)v.findViewById(R.id.login_scheme); + spnrScheme.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String scheme = parent.getAdapter().getItem(position).toString(); + textHttpWarning.setVisibility(scheme.equals("https://") ? View.GONE : View.VISIBLE); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + spnrScheme.setSelection(1); // HTTPS + + // other input fields + editBaseURI = (EditText)v.findViewById(R.id.login_host_path); + editBaseURI.addTextChangedListener(this); + + editUserName = (EditText)v.findViewById(R.id.userName); + editUserName.addTextChangedListener(this); + + editPassword = (EditText)v.findViewById(R.id.password); + editPassword.addTextChangedListener(this); + + checkboxPreemptive = (CheckBox) v.findViewById(R.id.auth_preemptive); + + // hook into action bar + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + FragmentTransaction ft = getFragmentManager().beginTransaction(); + + Bundle args = new Bundle(); + try { + args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, getBaseURI().toString()); + } catch (URISyntaxException e) { + } + 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); + dialog.show(ft, QueryServerDialogFragment.class.getName()); + break; + default: + return false; + } + return true; + } + + + private URI getBaseURI() throws URISyntaxException { + String scheme = spnrScheme.getSelectedItem().toString(), + host_path = editBaseURI.getText().toString(); + return new URI(scheme + host_path); + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean usernameOk = editUserName.getText().length() > 0, + passwordOk = editPassword.getText().length() > 0, + urlOk = false; + + // check host name + try { + if (!StringUtils.isBlank(getBaseURI().getHost())) + urlOk = true; + } catch (Exception e) { + } + + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(usernameOk && passwordOk && urlOk); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java new file mode 100644 index 00000000..10f64d83 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import lombok.Cleanup; +import android.app.DialogFragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.content.Loader; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.Toast; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.DavResourceFinder; +import at.bitfire.davdroid.resource.ServerInfo; +import at.bitfire.davdroid.webdav.DavException; +import org.apache.http.HttpException; + +public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks { + private static final String TAG = "davdroid.QueryServerDialogFragment"; + public static final String + EXTRA_BASE_URI = "base_uri", + EXTRA_USER_NAME = "user_name", + EXTRA_PASSWORD = "password", + EXTRA_AUTH_PREEMPTIVE = "auth_preemptive"; + + ProgressBar progressBar; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog); + setCancelable(false); + + Loader loader = getLoaderManager().initLoader(0, getArguments(), this); + if (savedInstanceState == null) // http://code.google.com/p/android/issues/detail?id=14944 + loader.forceLoad(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.query_server, container, false); + return v; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Log.i(TAG, "onCreateLoader"); + return new ServerInfoLoader(getActivity(), args); + } + + @Override + public void onLoadFinished(Loader loader, ServerInfo serverInfo) { + if (serverInfo.getErrorMessage() != null) + Toast.makeText(getActivity(), serverInfo.getErrorMessage(), Toast.LENGTH_LONG).show(); + else { + SelectCollectionsFragment selectCollections = new SelectCollectionsFragment(); + Bundle arguments = new Bundle(); + arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo); + selectCollections.setArguments(arguments); + + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, selectCollections) + .addToBackStack(null) + .commitAllowingStateLoss(); + } + + getDialog().dismiss(); + } + + @Override + public void onLoaderReset(Loader arg0) { + } + + + static class ServerInfoLoader extends AsyncTaskLoader { + private static final String TAG = "davdroid.ServerInfoLoader"; + final Bundle args; + final Context context; + + public ServerInfoLoader(Context context, Bundle args) { + super(context); + this.context = context; + this.args = args; + } + + @Override + public ServerInfo loadInBackground() { + ServerInfo serverInfo = new ServerInfo( + URI.create(args.getString(EXTRA_BASE_URI)), + args.getString(EXTRA_USER_NAME), + args.getString(EXTRA_PASSWORD), + args.getBoolean(EXTRA_AUTH_PREEMPTIVE) + ); + + try { + @Cleanup DavResourceFinder finder = new DavResourceFinder(context); + finder.findResources(serverInfo); + } catch (URISyntaxException e) { + serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage())); + } catch (IOException e) { + serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage())); + } catch (HttpException e) { + Log.e(TAG, "HTTP error while querying server info", e); + serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage())); + } catch (DavException e) { + Log.e(TAG, "DAV error while querying server info", e); + serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage())); + } + + return serverInfo; + } + + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java new file mode 100644 index 00000000..95c871ed --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.syncadapter; + +import lombok.Getter; +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckedTextView; +import android.widget.ListAdapter; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.ServerInfo; +import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo.Type; + +public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter { + final static int TYPE_ADDRESS_BOOKS_HEADING = 0, + TYPE_ADDRESS_BOOKS_ROW = 1, + TYPE_CALENDARS_HEADING = 2, + TYPE_CALENDARS_ROW = 3; + + protected Context context; + protected ServerInfo serverInfo; + @Getter protected int nAddressBooks, nCalendars; + + + public SelectCollectionsAdapter(Context context, ServerInfo serverInfo) { + this.context = context; + + this.serverInfo = serverInfo; + nAddressBooks = (serverInfo.getAddressBooks() == null) ? 0 : serverInfo.getAddressBooks().size(); + nCalendars = (serverInfo.getCalendars() == null) ? 0 : serverInfo.getCalendars().size(); + } + + + // item data + + @Override + public int getCount() { + return nAddressBooks + nCalendars + 2; + } + + @Override + public Object getItem(int position) { + if (position > 0 && position <= nAddressBooks) + return serverInfo.getAddressBooks().get(position - 1); + else if (position > nAddressBooks + 1) + return serverInfo.getCalendars().get(position - nAddressBooks - 2); + return null; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + return position; + } + + + // item views + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) + return TYPE_ADDRESS_BOOKS_HEADING; + else if (position <= nAddressBooks) + return TYPE_ADDRESS_BOOKS_ROW; + else if (position == nAddressBooks + 1) + return TYPE_CALENDARS_HEADING; + else if (position <= nAddressBooks + nCalendars + 1) + return TYPE_CALENDARS_ROW; + else + return IGNORE_ITEM_VIEW_TYPE; + } + + @Override + @SuppressLint("InflateParams") + public View getView(int position, View convertView, ViewGroup parent) { + View v = convertView; + + // step 1: get view (either by creating or recycling) + if (v == null) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (getItemViewType(position)) { + case TYPE_ADDRESS_BOOKS_HEADING: + v = inflater.inflate(R.layout.address_books_heading, parent, false); + break; + case TYPE_ADDRESS_BOOKS_ROW: + v = inflater.inflate(android.R.layout.simple_list_item_single_choice, null); + v.setPadding(0, 8, 0, 8); + break; + case TYPE_CALENDARS_HEADING: + v = inflater.inflate(R.layout.calendars_heading, parent, false); + break; + case TYPE_CALENDARS_ROW: + v = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null); + v.setPadding(0, 8, 0, 8); + } + } + + // step 2: fill view with content + switch (getItemViewType(position)) { + case TYPE_ADDRESS_BOOKS_ROW: + setContent((CheckedTextView)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position)); + break; + case TYPE_CALENDARS_ROW: + setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position)); + } + + return v; + } + + protected void setContent(CheckedTextView view, int collectionIcon, ServerInfo.ResourceInfo info) { + // set layout and icons + view.setCompoundDrawablesWithIntrinsicBounds(collectionIcon, 0, info.isReadOnly() ? R.drawable.ic_read_only : 0, 0); + view.setCompoundDrawablePadding(10); + + // set text + String title = info.getTitle(); + if (title == null) // unnamed collection + title = context.getString((info.getType() == Type.ADDRESS_BOOK) ? + R.string.setup_address_book : R.string.setup_calendar); + title = "" + title + ""; + if (info.isReadOnly()) + title = title + " (" + context.getString(R.string.setup_read_only) + ")"; + + String description = info.getDescription(); + if (description == null) + description = info.getURL(); + + // FIXME escape HTML + view.setText(Html.fromHtml(title + "
" + description)); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + int type = getItemViewType(position); + return (type == TYPE_ADDRESS_BOOKS_ROW || type == TYPE_CALENDARS_ROW); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java new file mode 100644 index 00000000..c7a044b2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java @@ -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.syncadapter; + +import android.app.ListFragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListAdapter; +import android.widget.ListView; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.ServerInfo; + +public class SelectCollectionsFragment extends ListFragment { + public static final String KEY_SERVER_INFO = "server_info"; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = super.onCreateView(inflater, container, savedInstanceState); + setHasOptionsMenu(true); + return v; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final ListView listView = getListView(); + listView.setPadding(20, 30, 20, 30); + + View header = getActivity().getLayoutInflater().inflate(R.layout.select_collections_header, getListView(), false); + listView.addHeaderView(header, getListView(), false); + + final ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + final SelectCollectionsAdapter adapter = new SelectCollectionsAdapter(view.getContext(), serverInfo); + setListAdapter(adapter); + + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + int itemPosition = position - 1; // one list header view at pos. 0 + if (adapter.getItemViewType(itemPosition) == SelectCollectionsAdapter.TYPE_ADDRESS_BOOKS_ROW) { + // unselect all other address books + for (int pos = 1; pos <= adapter.getNAddressBooks(); pos++) + if (pos != itemPosition) + listView.setItemChecked(pos + 1, false); + } + + getActivity().invalidateOptionsMenu(); + } + }); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + + // synchronize only selected collections + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + addressBook.setEnabled(false); + for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars()) + calendar.setEnabled(false); + + ListAdapter adapter = getListView().getAdapter(); + for (long id : getListView().getCheckedItemIds()) { + int position = (int)id + 1; // +1 because header view is inserted at pos. 0 + ServerInfo.ResourceInfo info = (ServerInfo.ResourceInfo)adapter.getItem(position); + info.setEnabled(true); + } + + // pass to "account details" fragment + AccountDetailsFragment accountDetails = new AccountDetailsFragment(); + Bundle arguments = new Bundle(); + arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo); + accountDetails.setArguments(arguments); + + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, accountDetails) + .addToBackStack(null) + .commitAllowingStateLoss(); + break; + default: + return false; + } + return true; + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean ok = false; + try { + ok = getListView().getCheckedItemCount() > 0; + } catch(IllegalStateException e) { + } + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(ok); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java new file mode 100644 index 00000000..c1bf2fa2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * 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.syncadapter; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; + +import net.fortuna.ical4j.model.ValidationException; +import android.content.SyncResult; +import android.util.Log; +import at.bitfire.davdroid.ArrayUtils; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.RecordNotFoundException; +import at.bitfire.davdroid.resource.RemoteCollection; +import at.bitfire.davdroid.resource.Resource; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.HttpException; +import at.bitfire.davdroid.webdav.NotFoundException; +import at.bitfire.davdroid.webdav.PreconditionFailedException; + +public class SyncManager { + private static final String TAG = "davdroid.SyncManager"; + + private static final int MAX_MULTIGET_RESOURCES = 35; + + protected LocalCollection local; + protected RemoteCollection remote; + + + public SyncManager(LocalCollection local, RemoteCollection remote) { + this.local = local; + this.remote = remote; + } + + + public void synchronize(boolean manualSync, SyncResult syncResult) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + // PHASE 1: push local changes to server + int deletedRemotely = pushDeleted(), + addedRemotely = pushNew(), + updatedRemotely = pushDirty(); + + syncResult.stats.numEntries = deletedRemotely + addedRemotely + updatedRemotely; + + // PHASE 2A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed) + boolean fetchCollection = syncResult.stats.numEntries > 0; + if (manualSync) { + Log.i(TAG, "Synchronization forced"); + fetchCollection = true; + } + if (!fetchCollection) { + String currentCTag = remote.getCTag(), + lastCTag = local.getCTag(); + Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag); + if (currentCTag == null || !currentCTag.equals(lastCTag)) + fetchCollection = true; + } + + if (!fetchCollection) { + Log.i(TAG, "No local changes and CTags match, no need to sync"); + return; + } + + // PHASE 2B: detect details of remote changes + Log.i(TAG, "Fetching remote resource list"); + Set remotelyAdded = new HashSet(), + remotelyUpdated = new HashSet(); + + Resource[] remoteResources = remote.getMemberETags(); + for (Resource remoteResource : remoteResources) { + try { + Resource localResource = local.findByRemoteName(remoteResource.getName(), false); + if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag())) + remotelyUpdated.add(remoteResource); + } catch(RecordNotFoundException e) { + remotelyAdded.add(remoteResource); + } + } + + // PHASE 3: pull remote changes from server + syncResult.stats.numInserts = pullNew(remotelyAdded.toArray(new Resource[0])); + syncResult.stats.numUpdates = pullChanged(remotelyUpdated.toArray(new Resource[0])); + syncResult.stats.numEntries += syncResult.stats.numInserts + syncResult.stats.numUpdates; + + Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore"); + local.deleteAllExceptRemoteNames(remoteResources); + local.commit(); + + // update collection CTag + Log.i(TAG, "Sync complete, fetching new CTag"); + local.setCTag(remote.getCTag()); + } + + + private int pushDeleted() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] deletedIDs = local.findDeleted(); + + try { + Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)"); + for (long id : deletedIDs) + try { + Resource res = local.findById(id, false); + if (res.getName() != null) // is this resource even present remotely? + try { + remote.delete(res); + } catch(NotFoundException e) { + Log.i(TAG, "Locally-deleted resource has already been removed from server"); + } catch(PreconditionFailedException e) { + Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile"); + } + + // always delete locally so that the record with the DELETED flag doesn't cause another deletion attempt + local.delete(res); + + count++; + } catch (RecordNotFoundException e) { + Log.wtf(TAG, "Couldn't read locally-deleted record", e); + } + } finally { + local.commit(); + } + return count; + } + + private int pushNew() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] newIDs = local.findNew(); + Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)"); + try { + for (long id : newIDs) + try { + Resource res = local.findById(id, true); + String eTag = remote.add(res); + if (eTag != null) + local.updateETag(res, eTag); + local.clearDirty(res); + count++; + } catch(PreconditionFailedException e) { + Log.i(TAG, "Didn't overwrite existing resource with other content"); + } catch (ValidationException e) { + Log.e(TAG, "Couldn't create entity for adding: " + e.toString()); + } catch (RecordNotFoundException e) { + Log.wtf(TAG, "Couldn't read new record", e); + } + } finally { + local.commit(); + } + return count; + } + + private int pushDirty() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] dirtyIDs = local.findUpdated(); + Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)"); + try { + for (long id : dirtyIDs) { + try { + Resource res = local.findById(id, true); + String eTag = remote.update(res); + if (eTag != null) + local.updateETag(res, eTag); + local.clearDirty(res); + count++; + } catch(PreconditionFailedException e) { + Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile"); + } catch (ValidationException e) { + Log.e(TAG, "Couldn't create entity for updating: " + e.toString()); + } catch (RecordNotFoundException e) { + Log.e(TAG, "Couldn't read dirty record", e); + } + } + } finally { + local.commit(); + } + return count; + } + + private int pullNew(Resource[] resourcesToAdd) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + int count = 0; + Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)"); + + for (Resource[] resources : ArrayUtils.partition(resourcesToAdd, MAX_MULTIGET_RESOURCES)) + for (Resource res : remote.multiGet(resources)) { + Log.d(TAG, "Adding " + res.getName()); + local.add(res); + local.commit(); + count++; + } + return count; + } + + private int pullChanged(Resource[] resourcesToUpdate) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + int count = 0; + Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)"); + + for (Resource[] resources : ArrayUtils.partition(resourcesToUpdate, MAX_MULTIGET_RESOURCES)) + for (Resource res : remote.multiGet(resources)) { + Log.i(TAG, "Updating " + res.getName()); + local.updateByRemoteName(res); + local.commit(); + count++; + } + return count; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java new file mode 100644 index 00000000..9263874d --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java @@ -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.syncadapter; + +import java.util.List; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import at.bitfire.davdroid.webdav.WebDavResource; + +public class WebDavResourceAdapter extends BaseAdapter { + protected int viewId; + protected LayoutInflater inflater; + WebDavResource[] items; + + public WebDavResourceAdapter(Context context, int textViewResourceId, List objects) { + viewId = textViewResourceId; + inflater = LayoutInflater.from(context); + items = objects.toArray(new WebDavResource[0]); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + WebDavResource item = items[position]; + View itemView = (View)inflater.inflate(viewId, null); + + TextView textName = (TextView) itemView.findViewById(android.R.id.text1); + textName.setText(item.getDisplayName()); + + TextView textDescription = (TextView) itemView.findViewById(android.R.id.text2); + String description = item.getDescription(); + if (description == null) + description = item.getLocation().getPath(); + textDescription.setText(description); + + return itemView; + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public Object getItem(int position) { + return items[position]; + } + + @Override + public long getItemId(int position) { + return position; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java new file mode 100644 index 00000000..9b32960f --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 org.simpleframework.xml.Namespace; +import org.simpleframework.xml.NamespaceList; +import org.simpleframework.xml.Root; + +@Root(name="addressbook-multiget") +@NamespaceList({ + @Namespace(reference="DAV:"), + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") +}) +@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") +public class DavAddressbookMultiget extends DavMultiget { +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java new file mode 100644 index 00000000..75a7ce71 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 org.simpleframework.xml.Namespace; +import org.simpleframework.xml.NamespaceList; +import org.simpleframework.xml.Root; + +@Root(name="calendar-multiget") +@NamespaceList({ + @Namespace(reference="DAV:"), + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") +}) +@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") +public class DavCalendarMultiget extends DavMultiget { +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java new file mode 100644 index 00000000..506af6da --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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; + +public class DavException extends Exception { + private static final long serialVersionUID = -2118919144443165706L; + + final private static String prefix = "Invalid DAV response: "; + + /* used to indiciate DAV protocol errors */ + + + public DavException(String message) { + super(prefix + message); + } + + public DavException(String message, Throwable ex) { + super(prefix + message, ex); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java new file mode 100644 index 00000000..438c4e49 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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 org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +@Root(name="href") +@Namespace(prefix="D",reference="DAV:") +public class DavHref { + @Text + String href; + + DavHref() { + } + + public DavHref(String href) { + this.href = href; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java new file mode 100644 index 00000000..9a20e6f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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 android.util.Log; +import at.bitfire.davdroid.Constants; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; + +import java.lang.reflect.Method; + + +public class DavHttpClient { + private final static String TAG = "davdroid.DavHttpClient"; + + private final static RequestConfig defaultRqConfig; + private final static Registry socketFactoryRegistry; + + static { + socketFactoryRegistry = RegistryBuilder. create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", TlsSniSocketFactory.INSTANCE) + .build(); + + // use request defaults from AndroidHttpClient + defaultRqConfig = RequestConfig.copy(RequestConfig.DEFAULT) + .setConnectTimeout(20*1000) + .setSocketTimeout(45*1000) + .setStaleConnectionCheckEnabled(false) + .build(); + } + + + public static CloseableHttpClient create(boolean disableCompression, boolean logTraffic) { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + // limits per DavHttpClient (= per DavSyncAdapter extends AbstractThreadedSyncAdapter) + connectionManager.setMaxTotal(3); // max. 3 connections in total + connectionManager.setDefaultMaxPerRoute(2); // max. 2 connections per host + + HttpClientBuilder builder = HttpClients.custom() + .useSystemProperties() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(defaultRqConfig) + .setRetryHandler(DavHttpRequestRetryHandler.INSTANCE) + .setRedirectStrategy(DavRedirectStrategy.INSTANCE) + .setUserAgent("DAVdroid/" + Constants.APP_VERSION) + .disableCookieManagement(); + + if (Log.isLoggable("Wire", Log.DEBUG)) { + Log.i(TAG, "Wire logging active, disabling HTTP compression"); + builder = builder.disableContentCompression(); + } + + return builder.build(); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java new file mode 100644 index 00000000..fdbcc05c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.util.Locale; + +import org.apache.commons.lang.ArrayUtils; + +import org.apache.http.HttpRequest; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; + +public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler { + final static DavHttpRequestRetryHandler INSTANCE = new DavHttpRequestRetryHandler(); + + // see http://www.iana.org/assignments/http-methods/http-methods.xhtml + private final static String idempotentMethods[] = { + "DELETE", "GET", "HEAD", "MKCALENDAR", "MKCOL", "OPTIONS", "PROPFIND", "PROPPATCH", + "PUT", "REPORT", "SEARCH", "TRACE" + }; + + public DavHttpRequestRetryHandler() { + super(/* retry count */ 3, /* retry already sent requests? */ false); + } + + //@Override + protected boolean handleAsIdempotent(final HttpRequest request) { + final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT); + return ArrayUtils.contains(idempotentMethods, method); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java new file mode 100644 index 00000000..794865ec --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java @@ -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.webdav; + +public class DavIncapableException extends DavException { + private static final long serialVersionUID = -7199786680939975667L; + + /* used to indicate that the server doesn't support DAV */ + + public DavIncapableException(String msg) { + super(msg); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java new file mode 100644 index 00000000..64162b19 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * 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.util.ArrayList; +import java.util.List; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Order; + +@Order(elements={"prop","href"}) +public class DavMultiget { + public enum Type { + ADDRESS_BOOK, + CALENDAR + } + + @Element + DavProp prop; + + @ElementList(inline=true) + List hrefs; + + + public static DavMultiget newRequest(Type type, String names[]) { + DavMultiget multiget = (type == Type.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget(); + + multiget.prop = new DavProp(); + multiget.prop.getetag = new DavProp.GetETag(); + + if (type == Type.ADDRESS_BOOK) + multiget.prop.addressData = new DavProp.AddressData(); + else if (type == Type.CALENDAR) + multiget.prop.calendarData = new DavProp.CalendarData(); + + multiget.hrefs = new ArrayList(names.length); + for (String name : names) + multiget.hrefs.add(new DavHref(name)); + + return multiget; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java new file mode 100644 index 00000000..dce27c2b --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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.util.List; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; + +@Namespace(reference="DAV:") +@Root(strict=false) +public class DavMultistatus { + @ElementList(inline=true,entry="response",required=false) + List response; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java new file mode 100644 index 00000000..6e3464b9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java @@ -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.webdav; + +public class DavNoContentException extends DavException { + private static final long serialVersionUID = 6256645020350945477L; + + private final static String message = "HTTP response entity (content) expected but not received"; + + public DavNoContentException() { + super(message); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java new file mode 100644 index 00000000..07eb6e18 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java @@ -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.webdav; + +public class DavNoMultiStatusException extends DavException { + private static final long serialVersionUID = -3600405724694229828L; + + private final static String message = "207 Multi-Status expected but not received"; + + public DavNoMultiStatusException() { + super(message); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java new file mode 100644 index 00000000..93f25645 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java @@ -0,0 +1,211 @@ +/******************************************************************************* + * 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.util.List; + +import lombok.Getter; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +@Namespace(prefix="D",reference="DAV:") +@Root(strict=false) +public class DavProp { + + /* RFC 4918 WebDAV */ + + @Element(required=false) + ResourceType resourcetype; + + @Element(required=false) + DisplayName displayname; + + @Element(required=false) + GetCTag getctag; + + @Element(required=false) + GetETag getetag; + + @Root(strict=false) + public static class ResourceType { + @Element(required=false) + @Getter private Collection collection; + public static class Collection { } + + @Element(required=false) + @Getter private Addressbook addressbook; + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class Addressbook { } + + @Element(required=false) + @Getter private Calendar calendar; + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class Calendar { } + } + + public static class DisplayName { + @Text(required=false) + @Getter private String displayName; + } + + @Namespace(prefix="CS",reference="http://calendarserver.org/ns/") + public static class GetCTag { + @Text(required=false) + @Getter private String CTag; + } + + public static class GetETag { + @Text(required=false) + @Getter private String ETag; + } + + + /* RFC 5397 WebDAV Current Principal Extension */ + + @Element(required=false,name="current-user-principal") + CurrentUserPrincipal currentUserPrincipal; + + public static class CurrentUserPrincipal { + @Element(required=false) + @Getter private DavHref href; + } + + + /* RFC 3744 WebDAV Access Control Protocol */ + + @ElementList(required=false,name="current-user-privilege-set",entry="privilege") + List currentUserPrivilegeSet; + + public static class Privilege { + @Element(required=false) + @Getter private PrivAll all; + + @Element(required=false) + @Getter private PrivBind bind; + + @Element(required=false) + @Getter private PrivUnbind unbind; + + @Element(required=false) + @Getter private PrivWrite write; + + @Element(required=false,name="write-content") + @Getter private PrivWriteContent writeContent; + + public static class PrivAll { } + public static class PrivBind { } + public static class PrivUnbind { } + public static class PrivWrite { } + public static class PrivWriteContent { } + } + + + + /* RFC 4791 CalDAV, RFC 6352 CardDAV */ + + @Element(required=false,name="addressbook-home-set") + AddressbookHomeSet addressbookHomeSet; + + @Element(required=false,name="calendar-home-set") + CalendarHomeSet calendarHomeSet; + + @Element(required=false,name="addressbook-description") + AddressbookDescription addressbookDescription; + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + @ElementList(required=false,name="supported-address-data",entry="address-data-type") + List supportedAddressData; + + @Element(required=false,name="calendar-description") + CalendarDescription calendarDescription; + + @Element(required=false,name="calendar-color") + CalendarColor calendarColor; + + @Element(required=false,name="calendar-timezone") + CalendarTimezone calendarTimezone; + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + @ElementList(required=false,name="supported-calendar-component-set",entry="comp") + List supportedCalendarComponentSet; + + @Element(name="address-data",required=false) + AddressData addressData; + + @Element(name="calendar-data",required=false) + CalendarData calendarData; + + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressbookHomeSet { + @Element(required=false) + @Getter private DavHref href; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarHomeSet { + @Element(required=false) + @Getter private DavHref href; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressbookDescription { + @Text(required=false) + @Getter private String description; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressDataType { + @Attribute(name="content-type") + @Getter private String contentType; + + @Attribute + @Getter private String version; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarDescription { + @Text(required=false) + @Getter private String description; + } + + @Namespace(prefix="A",reference="http://apple.com/ns/ical/") + public static class CalendarColor { + @Text(required=false) + @Getter private String color; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarTimezone { + @Text(required=false) + @Getter private String timezone; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class Comp { + @Attribute + @Getter String name; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressData { + @Text(required=false) + @Getter String vcard; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarData { + @Text(required=false) + @Getter String ical; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java new file mode 100644 index 00000000..abf4104e --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * 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 org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; + +@Namespace(reference="DAV:") +@Root(name="propfind") +public class DavPropfind { + @Element(required=false) + protected DavProp prop; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java new file mode 100644 index 00000000..74ee66d6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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 org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +@Root(strict=false,name="propstat") +public class DavPropstat { + @Element + DavProp prop; + + @Element + String status; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java new file mode 100644 index 00000000..6e4e7d47 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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.net.URI; +import java.net.URISyntaxException; + +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; +import org.apache.http.RequestLine; +import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; + +/** + * Custom Redirect Strategy that handles 30x for CalDAV/CardDAV-specific requests correctly + */ +public class DavRedirectStrategy implements RedirectStrategy { + private final static String TAG = "davdroid.DavRedirectStrategy"; + final static DavRedirectStrategy INSTANCE = new DavRedirectStrategy(); + + protected final static String REDIRECTABLE_METHODS[] = { + "OPTIONS", "GET", "PUT", "DELETE" + }; + + + @Override + public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { + RequestLine line = request.getRequestLine(); + + String location = getLocation(request, response, context).toString(); + Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location); + + return RequestBuilder.copy(request) + .setUri(location) + .removeHeaders("Content-Length") // Content-Length will be set again automatically, if required; + // remove it now to avoid duplicate header + .build(); + } + + /** + * Determines whether a response indicates a redirection and if it does, whether to follow this redirection. + * PROPFIND and REPORT must handle redirections explicitely because multi-status processing requires knowledge of the content location. + * @return true for 3xx responses on OPTIONS, GET, PUT, DELETE requests that have a valid Location header; false otherwise + */ + @Override + public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { + if (response.getStatusLine().getStatusCode()/100 == 3) { + boolean redirectable = false; + for (String method : REDIRECTABLE_METHODS) + if (method.equalsIgnoreCase(request.getRequestLine().getMethod())) { + redirectable = true; + break; + } + return redirectable && getLocation(request, response, context) != null; + } + return false; + } + + /** + * Gets the destination of a redirection + * @return absolute URL of new location; null if not available + */ + static URI getLocation(HttpRequest request, HttpResponse response, HttpContext context) { + Header locationHdr = response.getFirstHeader("Location"); + if (locationHdr == null) { + Log.e(TAG, "Received redirection without Location header, ignoring"); + return null; + } + try { + URI location = URIUtils.parseURI(locationHdr.getValue()); + + // some servers don't return absolute URLs as required by RFC 2616 + if (!location.isAbsolute()) { + Log.w(TAG, "Received invalid redirection to relative URL, repairing"); + URI originalURI = URIUtils.parseURI(request.getRequestLine().getUri()); + if (!originalURI.isAbsolute()) { + final HttpHost target = HttpClientContext.adapt(context).getTargetHost(); + if (target != null) + originalURI = org.apache.http.client.utils.URIUtils.rewriteURI(originalURI, target); + else + return null; + } + return originalURI.resolve(location); + } + return location; + } catch (URISyntaxException e) { + Log.e(TAG, "Received redirection from/to invalid URI, ignoring", e); + } + return null; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java new file mode 100644 index 00000000..b1ba1fd9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.util.List; + +import lombok.Getter; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +@Root(strict=false) +public class DavResponse { + @Element + @Getter DavHref href; + + @ElementList(inline=true) + @Getter List propstat; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java new file mode 100644 index 00000000..79b48164 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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 lombok.Getter; + +public class HttpException extends org.apache.http.HttpException { + private static final long serialVersionUID = -4805778240079377401L; + + @Getter private int code; + + HttpException(int code, String message) { + super(message); + this.code = code; + } + + public boolean isClientError() { + return code/100 == 4; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java new file mode 100644 index 00000000..677905ed --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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.StringWriter; +import java.net.URI; +import java.util.LinkedList; + +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; + +import android.util.Log; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.StringEntity; + +public class HttpPropfind extends HttpEntityEnclosingRequestBase { + private static final String TAG = "davdroid.HttpPropfind"; + + public final static String METHOD_NAME = "PROPFIND"; + + public enum Mode { + CURRENT_USER_PRINCIPAL, + HOME_SETS, + CARDDAV_COLLECTIONS, + CALDAV_COLLECTIONS, + COLLECTION_CTAG, + MEMBERS_ETAG + } + + + HttpPropfind(URI uri) { + setURI(uri); + } + + HttpPropfind(URI uri, Mode mode) { + this(uri); + + DavPropfind propfind = new DavPropfind(); + propfind.prop = new DavProp(); + + int depth = 0; + switch (mode) { + case CURRENT_USER_PRINCIPAL: + propfind.prop.currentUserPrincipal = new DavProp.CurrentUserPrincipal(); + break; + case HOME_SETS: + propfind.prop.addressbookHomeSet = new DavProp.AddressbookHomeSet(); + propfind.prop.calendarHomeSet = new DavProp.CalendarHomeSet(); + break; + case CARDDAV_COLLECTIONS: + depth = 1; + propfind.prop.displayname = new DavProp.DisplayName(); + propfind.prop.resourcetype = new DavProp.ResourceType(); + propfind.prop.currentUserPrivilegeSet = new LinkedList(); + propfind.prop.addressbookDescription = new DavProp.AddressbookDescription(); + propfind.prop.supportedAddressData = new LinkedList(); + break; + case CALDAV_COLLECTIONS: + depth = 1; + propfind.prop.displayname = new DavProp.DisplayName(); + propfind.prop.resourcetype = new DavProp.ResourceType(); + propfind.prop.currentUserPrivilegeSet = new LinkedList(); + propfind.prop.calendarDescription = new DavProp.CalendarDescription(); + propfind.prop.calendarColor = new DavProp.CalendarColor(); + propfind.prop.calendarTimezone = new DavProp.CalendarTimezone(); + propfind.prop.supportedCalendarComponentSet = new LinkedList(); + break; + case COLLECTION_CTAG: + propfind.prop.getctag = new DavProp.GetCTag(); + break; + case MEMBERS_ETAG: + depth = 1; + propfind.prop.getctag = new DavProp.GetCTag(); + propfind.prop.getetag = new DavProp.GetETag(); + break; + } + + try { + Serializer serializer = new Persister(); + StringWriter writer = new StringWriter(); + serializer.write(propfind, writer); + + setHeader("Content-Type", "text/xml; charset=UTF-8"); + setHeader("Accept", "text/xml"); + setHeader("Depth", String.valueOf(depth)); + setEntity(new StringEntity(writer.toString(), "UTF-8")); + } catch(Exception ex) { + Log.e(TAG, "Couldn't prepare PROPFIND request for " + uri, ex); + abort(); + } + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java new file mode 100644 index 00000000..d7323818 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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 android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.StringEntity; + +public class HttpReport extends HttpEntityEnclosingRequestBase { + private static final String TAG = "davdroid.HttpEntityEncloseRequestBase"; + + public final static String METHOD_NAME = "REPORT"; + + + HttpReport(URI uri) { + setURI(uri); + } + + HttpReport(URI uri, String entity) { + this(uri); + + setHeader("Content-Type", "text/xml; charset=UTF-8"); + setHeader("Accept", "text/xml"); + setHeader("Depth", "0"); + + try { + setEntity(new StringEntity(entity, "UTF-8")); + } catch (UnsupportedEncodingException e) { + Log.wtf(TAG, "String entity doesn't support UTF-8"); + } + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java new file mode 100644 index 00000000..4eab6adb --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * 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 org.apache.http.HttpStatus; + +public class NotAuthorizedException extends HttpException { + private static final long serialVersionUID = 2490525047224413586L; + + public NotAuthorizedException(String reason) { + super(HttpStatus.SC_UNAUTHORIZED, reason); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java new file mode 100644 index 00000000..f612d733 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java @@ -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.webdav; + +import org.apache.http.HttpStatus; + +public class NotFoundException extends HttpException { + private static final long serialVersionUID = 1565961502781880483L; + + public NotFoundException(String reason) { + super(HttpStatus.SC_NOT_FOUND, reason); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java new file mode 100644 index 00000000..064ff38a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java @@ -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.webdav; + +import org.apache.http.HttpStatus; + +public class PreconditionFailedException extends HttpException { + private static final long serialVersionUID = 102282229174086113L; + + public PreconditionFailedException(String reason) { + super(HttpStatus.SC_PRECONDITION_FAILED, reason); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java new file mode 100644 index 00000000..3cd46f25 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * 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.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import org.apache.commons.lang.StringUtils; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.util.Log; +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.protocol.HttpContext; + +public class TlsSniSocketFactory implements LayeredConnectionSocketFactory { + private static final String TAG = "davdroid.SNISocketFactory"; + + final static TlsSniSocketFactory INSTANCE = new TlsSniSocketFactory(); + + private final static SSLCertificateSocketFactory sslSocketFactory = + (SSLCertificateSocketFactory)SSLCertificateSocketFactory.getDefault(0); + private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier(); + + + /* + For SSL connections without HTTP(S) proxy: + 1) createSocket() is called + 2) connectSocket() is called which creates a new SSL connection + 2a) SNI is set up, and then + 2b) the connection is established, hands are shaken and certificate/host name are verified + + Layered sockets are used with HTTP(S) proxies: + 1) a new plain socket is created by the HTTP library + 2) the plain socket is connected to http://proxy:8080 + 3) a CONNECT request is sent to the proxy and the response is parsed + 4) now, createLayeredSocket() is called which wraps an SSL socket around the proxy connection, + doing all the set-up and verfication + 4a) Because SSLSocket.createSocket(socket, ...) always does a handshake without allowing + to set up SNI before, *** SNI is not available for layered connections *** (unless + active by Android's defaults, which it isn't at the moment). + */ + + + @Override + public Socket createSocket(HttpContext context) throws IOException { + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(); + setReasonableEncryption(ssl); + return ssl; + } + + @Override + public Socket connectSocket(int timeout, Socket plain, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException { + Log.d(TAG, "Preparing direct SSL connection (without proxy) to " + host); + + // we'll rather use an SSLSocket directly + plain.close(); + + // create a plain SSL socket, but don't do hostname/certificate verification yet + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(remoteAddr.getAddress(), host.getPort()); + setReasonableEncryption(ssl); + + // connect, set SNI, shake hands, verify, print connection info + connectWithSNI(ssl, host.getHostName()); + + return ssl; + } + + @Override + public Socket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException, UnknownHostException { + Log.d(TAG, "Preparing layered SSL connection (over proxy) to " + host); + + // create a layered SSL socket, but don't do hostname/certificate verification yet + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true); + setReasonableEncryption(ssl); + + // already connected, but verify host name again and print some connection info + Log.w(TAG, "Setting SNI/TLSv1.2 will silently fail because the handshake is already done"); + connectWithSNI(ssl, host); + + return ssl; + } + + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void connectWithSNI(SSLSocket ssl, String host) throws SSLPeerUnverifiedException { + // - set SNI host name + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.d(TAG, "Using documented SNI with host name " + host); + sslSocketFactory.setHostname(ssl, host); + } else { + Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection"); + try { + java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class); + setHostnameMethod.invoke(ssl, host); + } catch (Exception e) { + Log.w(TAG, "SNI not useable", e); + } + } + + // verify hostname and certificate + SSLSession session = ssl.getSession(); + if (!hostnameVerifier.verify(host, session)) + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host); + + Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); + } + + + @SuppressLint("DefaultLocale") + private void setReasonableEncryption(SSLSocket ssl) { + // set reasonable SSL/TLS settings before the handshake + + // Android 5.0+ (API level21) provides reasonable default settings + // but it still allows SSLv3 + // https://developer.android.com/about/versions/android-5.0-changes.html#ssl + + // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0, if available) + // - remove all SSL versions (especially SSLv3) because they're insecure now + List protocols = new LinkedList(); + for (String protocol : ssl.getSupportedProtocols()) + if (!protocol.toUpperCase().contains("SSL")) + protocols.add(protocol); + Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", ")); + ssl.setEnabledProtocols(protocols.toArray(new String[0])); + + if (android.os.Build.VERSION.SDK_INT < 21) { + // choose secure cipher suites + List allowedCiphers = Arrays.asList(new String[] { + // allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory) + // TLS 1.2 + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256", + // maximum interoperability + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + // additionally + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + }); + + List availableCiphers = Arrays.asList(ssl.getSupportedCipherSuites()); + + // preferred ciphers = allowed Ciphers \ availableCiphers + HashSet preferredCiphers = new HashSet(allowedCiphers); + preferredCiphers.retainAll(availableCiphers); + + // add preferred ciphers to enabled ciphers + // for maximum security, preferred ciphers should *replace* enabled ciphers, + // but I guess for the security level of DAVdroid, disabling of insecure + // ciphers should be a server-side task + HashSet enabledCiphers = preferredCiphers; + enabledCiphers.addAll(new HashSet(Arrays.asList(ssl.getEnabledCipherSuites()))); + + Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", ")); + ssl.setEnabledCipherSuites(enabledCiphers.toArray(new String[0])); + } + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java new file mode 100644 index 00000000..5643f8e4 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java @@ -0,0 +1,581 @@ +/******************************************************************************* + * 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.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import lombok.Cleanup; +import lombok.Getter; +import lombok.ToString; + +import org.apache.commons.lang.StringUtils; +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; + +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import at.bitfire.davdroid.resource.Event; +import at.bitfire.davdroid.webdav.DavProp.Comp; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicLineParser; +import org.apache.http.util.EntityUtils; +import ezvcard.VCardVersion; + + +/** + * Represents a WebDAV resource (file or collection). + * This class is used for all CalDAV/CardDAV communcation. + */ +@ToString +public class WebDavResource { + private static final String TAG = "davdroid.WebDavResource"; + + public enum Property { + CURRENT_USER_PRINCIPAL, // resource detection + ADDRESSBOOK_HOMESET, CALENDAR_HOMESET, + CONTENT_TYPE, READ_ONLY, // WebDAV (common) + DISPLAY_NAME, DESCRIPTION, ETAG, + IS_COLLECTION, CTAG, // collections + IS_CALENDAR, COLOR, TIMEZONE, // CalDAV + IS_ADDRESSBOOK, VCARD_VERSION // CardDAV + } + public enum PutMode { + ADD_DONT_OVERWRITE, + UPDATE_DONT_OVERWRITE + } + + // location of this resource + @Getter protected URI location; + + // DAV capabilities (DAV: header) and allowed DAV methods (set for OPTIONS request) + protected Set capabilities = new HashSet(), + methods = new HashSet(); + + // DAV properties + protected HashMap properties = new HashMap(); + @Getter protected List supportedComponents; + + // list of members (only for collections) + @Getter protected List members; + + // content (available after GET) + @Getter protected byte[] content; + + protected CloseableHttpClient httpClient; + protected HttpClientContext context; + + + public WebDavResource(CloseableHttpClient httpClient, URI baseURI) { + this.httpClient = httpClient; + location = baseURI; + + context = HttpClientContext.create(); + context.setCredentialsProvider(new BasicCredentialsProvider()); + } + + public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) { + this(httpClient, baseURI); + + HttpHost host = new HttpHost(baseURI.getHost(), baseURI.getPort(), baseURI.getScheme()); + context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + if (preemptive) { + Log.d(TAG, "Using preemptive authentication (not compatible with Digest auth)"); + AuthCache authCache = context.getAuthCache(); + if (authCache == null) + authCache = new BasicAuthCache(); + authCache.put(host, new BasicScheme()); + context.setAuthCache(authCache); + } + } + + WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings + httpClient = parent.httpClient; + context = parent.context; + location = parent.location; + } + + protected WebDavResource(WebDavResource parent, URI url) { + this(parent); + location = parent.location.resolve(url); + } + + public WebDavResource(WebDavResource parent, String member) throws URISyntaxException { + this(parent); + location = parent.location.resolve(URIUtils.parseURI(member)); + } + + public WebDavResource(WebDavResource parent, String member, String ETag) throws URISyntaxException { + this(parent, member); + properties.put(Property.ETAG, ETag); + } + + + + /* feature detection */ + + public void options() throws URISyntaxException, IOException, HttpException { + HttpOptions options = new HttpOptions(location); + CloseableHttpResponse response = httpClient.execute(options, context); + try { + checkResponse(response); + + Header[] allowHeaders = response.getHeaders("Allow"); + for (Header allowHeader : allowHeaders) + methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?"))); + + Header[] capHeaders = response.getHeaders("DAV"); + for (Header capHeader : capHeaders) + capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?"))); + } finally { + response.close(); + } + } + + public boolean supportsDAV(String capability) { + return capabilities.contains(capability); + } + + public boolean supportsMethod(String method) { + return methods.contains(method); + } + + + /* file hierarchy methods */ + + public String getName() { + String[] names = StringUtils.split(location.getPath(), "/"); + return names[names.length - 1]; + } + + + /* property methods */ + + public String getCurrentUserPrincipal() { + return properties.get(Property.CURRENT_USER_PRINCIPAL); + } + + public String getAddressbookHomeSet() { + return properties.get(Property.ADDRESSBOOK_HOMESET); + } + + public String getCalendarHomeSet() { + return properties.get(Property.CALENDAR_HOMESET); + } + + public String getContentType() { + return properties.get(Property.CONTENT_TYPE); + } + + public void setContentType(String mimeType) { + properties.put(Property.CONTENT_TYPE, mimeType); + } + + public boolean isReadOnly() { + return properties.containsKey(Property.READ_ONLY); + } + + public String getDisplayName() { + return properties.get(Property.DISPLAY_NAME); + } + + public String getDescription() { + return properties.get(Property.DESCRIPTION); + } + + public String getCTag() { + return properties.get(Property.CTAG); + } + public void invalidateCTag() { + properties.remove(Property.CTAG); + } + + public String getETag() { + return properties.get(Property.ETAG); + } + + public boolean isCalendar() { + return properties.containsKey(Property.IS_CALENDAR); + } + + public String getColor() { + return properties.get(Property.COLOR); + } + + public String getTimezone() { + return properties.get(Property.TIMEZONE); + } + + public boolean isAddressBook() { + return properties.containsKey(Property.IS_ADDRESSBOOK); + } + + public VCardVersion getVCardVersion() { + String versionStr = properties.get(Property.VCARD_VERSION); + return (versionStr != null) ? VCardVersion.valueOfByStr(versionStr) : null; + } + + + /* collection operations */ + + public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException { + CloseableHttpResponse response = null; + + // processMultiStatus() requires knowledge of the actual content location, + // so we have to handle redirections manually and create a new request for the new location + for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) { + HttpPropfind propfind = new HttpPropfind(location, mode); + response = httpClient.execute(propfind, context); + + if (response.getStatusLine().getStatusCode()/100 == 3) { + location = DavRedirectStrategy.getLocation(propfind, response, context); + Log.i(TAG, "Redirection on PROPFIND; trying again at new content URL: " + location); + // don't forget to throw away the unneeded response content + HttpEntity entity = response.getEntity(); + if (entity != null) { @Cleanup InputStream content = entity.getContent(); } + } else + break; // answer was NOT a redirection, continue + } + if (response == null) + throw new DavNoContentException(); + + try { + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); + } finally { + response.close(); + } + } + + public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException { + CloseableHttpResponse response = null; + + // processMultiStatus() requires knowledge of the actual content location, + // so we have to handle redirections manually and create a new request for the new location + for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) { + // build multi-get XML request + List hrefs = new LinkedList(); + for (String name : names) + hrefs.add(location.resolve(name).getPath()); + DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[0])); + + StringWriter writer = new StringWriter(); + try { + Serializer serializer = new Persister(); + serializer.write(multiget, writer); + } catch (Exception ex) { + Log.e(TAG, "Couldn't create XML multi-get request", ex); + throw new DavException("Couldn't create multi-get request"); + } + + // submit REPORT request + HttpReport report = new HttpReport(location, writer.toString()); + response = httpClient.execute(report, context); + + if (response.getStatusLine().getStatusCode()/100 == 3) { + location = DavRedirectStrategy.getLocation(report, response, context); + Log.i(TAG, "Redirection on REPORT multi-get; trying again at new content URL: " + location); + + // don't forget to throw away the unneeded response content + HttpEntity entity = response.getEntity(); + if (entity != null) { @Cleanup InputStream content = entity.getContent(); } + } else + break; // answer was NOT a redirection, continue + } + if (response == null) + throw new DavNoContentException(); + + try { + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); + } finally { + response.close(); + } + } + + + /* resource operations */ + + public void get(String acceptedType) throws URISyntaxException, IOException, HttpException, DavException { + HttpGet get = new HttpGet(location); + get.addHeader("Accept", acceptedType); + + CloseableHttpResponse response = httpClient.execute(get, context); + try { + checkResponse(response); + + HttpEntity entity = response.getEntity(); + if (entity == null) + throw new DavNoContentException(); + + content = EntityUtils.toByteArray(entity); + } finally { + response.close(); + } + } + + // returns the ETag of the created/updated resource, if available (null otherwise) + public String put(byte[] data, PutMode mode) throws URISyntaxException, IOException, HttpException { + HttpPut put = new HttpPut(location); + put.setEntity(new ByteArrayEntity(data)); + + switch (mode) { + case ADD_DONT_OVERWRITE: + put.addHeader("If-None-Match", "*"); + break; + case UPDATE_DONT_OVERWRITE: + put.addHeader("If-Match", (getETag() != null) ? getETag() : "*"); + break; + } + + if (getContentType() != null) + put.addHeader("Content-Type", getContentType()); + + CloseableHttpResponse response = httpClient.execute(put, context); + try { + checkResponse(response); + + Header eTag = response.getLastHeader("ETag"); + if (eTag != null) + return eTag.getValue(); + } finally { + response.close(); + } + + return null; + } + + public void delete() throws URISyntaxException, IOException, HttpException { + HttpDelete delete = new HttpDelete(location); + + if (getETag() != null) + delete.addHeader("If-Match", getETag()); + + CloseableHttpResponse response = httpClient.execute(delete, context); + try { + checkResponse(response); + } finally { + response.close(); + } + } + + + /* helpers */ + + protected void checkResponse(HttpResponse response) throws HttpException { + checkResponse(response.getStatusLine()); + + // handle Content-Location header (see RFC 4918 5.2 Collection Resources) + Header contentLocationHdr = response.getFirstHeader("Content-Location"); + if (contentLocationHdr != null) { + // Content-Location was set, update location correspondingly + location = location.resolve(contentLocationHdr.getValue()); + Log.d(TAG, "Set Content-Location to " + location); + } + } + + protected static void checkResponse(StatusLine statusLine) throws HttpException { + int code = statusLine.getStatusCode(); + + if (code/100 == 1 || code/100 == 2) // everything OK + return; + + String reason = code + " " + statusLine.getReasonPhrase(); + switch (code) { + case HttpStatus.SC_UNAUTHORIZED: + throw new NotAuthorizedException(reason); + case HttpStatus.SC_NOT_FOUND: + throw new NotFoundException(reason); + case HttpStatus.SC_PRECONDITION_FAILED: + throw new PreconditionFailedException(reason); + default: + throw new HttpException(code, reason); + } + } + + protected void processMultiStatus(HttpResponse response) throws IOException, HttpException, DavException { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS) + throw new DavNoMultiStatusException(); + + HttpEntity entity = response.getEntity(); + if (entity == null) + throw new DavNoContentException(); + @Cleanup InputStream content = entity.getContent(); + + DavMultistatus multiStatus; + try { + Serializer serializer = new Persister(); + multiStatus = serializer.read(DavMultistatus.class, content, false); + } catch (Exception ex) { + throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex); + } + + if (multiStatus.response == null) // empty response + throw new DavNoContentException(); + + // member list will be built from response + List members = new LinkedList(); + + // iterate through all resources (either ourselves or member) + for (DavResponse singleResponse : multiStatus.response) { + URI href; + try { + href = location.resolve(URIUtils.parseURI(singleResponse.getHref().href)); + } catch(Exception ex) { + Log.w(TAG, "Ignoring illegal member URI in multi-status response", ex); + continue; + } + Log.d(TAG, "Processing multi-status element: " + href); + + // process known properties + HashMap properties = new HashMap(); + List supportedComponents = null; + byte[] data = null; + + for (DavPropstat singlePropstat : singleResponse.getPropstat()) { + StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser()); + + // ignore information about missing properties etc. + if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2) + continue; + DavProp prop = singlePropstat.prop; + + if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null) + properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href); + + if (prop.currentUserPrivilegeSet != null) { + // privilege info available + boolean mayAll = false, + mayBind = false, + mayUnbind = false, + mayWrite = false, + mayWriteContent = false; + for (DavProp.Privilege privilege : prop.currentUserPrivilegeSet) { + if (privilege.getAll() != null) mayAll = true; + if (privilege.getBind() != null) mayBind = true; + if (privilege.getUnbind() != null) mayUnbind = true; + if (privilege.getWrite() != null) mayWrite = true; + if (privilege.getWriteContent() != null) mayWriteContent = true; + } + if (!mayAll && !mayWrite && !(mayWriteContent && mayBind && mayUnbind)) + properties.put(Property.READ_ONLY, "1"); + } + + if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null) + properties.put(Property.ADDRESSBOOK_HOMESET, URIUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href)); + + if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null) + properties.put(Property.CALENDAR_HOMESET, URIUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href)); + + if (prop.displayname != null) + properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName()); + + if (prop.resourcetype != null) { + if (prop.resourcetype.getCollection() != null) { + properties.put(Property.IS_COLLECTION, "1"); + // is a collection, ensure trailing slash + href = URIUtils.ensureTrailingSlash(href); + } + if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties + properties.put(Property.IS_ADDRESSBOOK, "1"); + + if (prop.addressbookDescription != null) + properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription()); + if (prop.supportedAddressData != null) + for (DavProp.AddressDataType dataType : prop.supportedAddressData) + if ("text/vcard".equalsIgnoreCase(dataType.getContentType())) + // ignore "3.0" as it MUST be supported anyway + if ("4.0".equals(dataType.getVersion())) + properties.put(Property.VCARD_VERSION, VCardVersion.V4_0.getVersion()); + } + if (prop.resourcetype.getCalendar() != null) { // CalDAV collection propertioes + properties.put(Property.IS_CALENDAR, "1"); + + if (prop.calendarDescription != null) + properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription()); + + if (prop.calendarColor != null) + properties.put(Property.COLOR, prop.calendarColor.getColor()); + + if (prop.calendarTimezone != null) + try { + properties.put(Property.TIMEZONE, Event.TimezoneDefToTzId(prop.calendarTimezone.getTimezone())); + } catch(IllegalArgumentException e) { + } + + if (prop.supportedCalendarComponentSet != null) { + supportedComponents = new LinkedList(); + for (Comp component : prop.supportedCalendarComponentSet) + supportedComponents.add(component.getName()); + } + } + } + + if (prop.getctag != null) + properties.put(Property.CTAG, prop.getctag.getCTag()); + + if (prop.getetag != null) + properties.put(Property.ETAG, prop.getetag.getETag()); + + if (prop.calendarData != null && prop.calendarData.ical != null) + data = prop.calendarData.ical.getBytes(); + else if (prop.addressData != null && prop.addressData.vcard != null) + data = prop.addressData.vcard.getBytes(); + } + + // about which resource is this response? + if (location.equals(href) || URIUtils.ensureTrailingSlash(location).equals(href)) { // about ourselves + this.properties.putAll(properties); + if (supportedComponents != null) + this.supportedComponents = supportedComponents; + this.content = data; + + } else { // about a member + WebDavResource member = new WebDavResource(this, href); + member.properties = properties; + member.supportedComponents = supportedComponents; + member.content = data; + + members.add(member); + } + } + + this.members = members; + } + +} diff --git a/app/src/main/java/ical4j.properties b/app/src/main/java/ical4j.properties new file mode 100644 index 00000000..6db152a4 --- /dev/null +++ b/app/src/main/java/ical4j.properties @@ -0,0 +1,6 @@ + +net.fortuna.ical4j.timezone.update.enabled=false + +ical4j.unfolding.relaxed=true +ical4j.parsing.relaxed=true +ical4j.compatibility.outlook=true diff --git a/app/src/main/res/drawable-hdpi/addressbook.png b/app/src/main/res/drawable-hdpi/addressbook.png new file mode 100644 index 00000000..79655934 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/addressbook.png differ diff --git a/app/src/main/res/drawable-hdpi/alerts_and_states_warning.png b/app/src/main/res/drawable-hdpi/alerts_and_states_warning.png new file mode 100644 index 00000000..a82ac4ac Binary files /dev/null and b/app/src/main/res/drawable-hdpi/alerts_and_states_warning.png differ diff --git a/app/src/main/res/drawable-hdpi/calendar.png b/app/src/main/res/drawable-hdpi/calendar.png new file mode 100644 index 00000000..3b5f92c3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/calendar.png differ diff --git a/app/src/main/res/drawable-hdpi/extra_actions_about.png b/app/src/main/res/drawable-hdpi/extra_actions_about.png new file mode 100644 index 00000000..e5582f4c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/extra_actions_about.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_new_account.png b/app/src/main/res/drawable-hdpi/ic_action_new_account.png new file mode 100644 index 00000000..9a41829f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_new_account.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_new_event.png b/app/src/main/res/drawable-hdpi/ic_action_new_event.png new file mode 100644 index 00000000..5a5d5587 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_new_event.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..dce11e1c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_read_only.png b/app/src/main/res/drawable-hdpi/ic_read_only.png new file mode 100644 index 00000000..aca46301 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_read_only.png differ diff --git a/app/src/main/res/drawable-hdpi/navigation_accept.png b/app/src/main/res/drawable-hdpi/navigation_accept.png new file mode 100644 index 00000000..58bf9721 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/navigation_accept.png differ diff --git a/app/src/main/res/drawable-hdpi/navigation_forward.png b/app/src/main/res/drawable-hdpi/navigation_forward.png new file mode 100644 index 00000000..812b3aaf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/navigation_forward.png differ diff --git a/app/src/main/res/drawable-hdpi/show_sync_settings.png b/app/src/main/res/drawable-hdpi/show_sync_settings.png new file mode 100644 index 00000000..4a6fb5b5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/show_sync_settings.png differ diff --git a/app/src/main/res/drawable-hdpi/view_website.png b/app/src/main/res/drawable-hdpi/view_website.png new file mode 100644 index 00000000..9edbb1b5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/view_website.png differ diff --git a/app/src/main/res/drawable-mdpi/addressbook.png b/app/src/main/res/drawable-mdpi/addressbook.png new file mode 100644 index 00000000..4779b7dd Binary files /dev/null and b/app/src/main/res/drawable-mdpi/addressbook.png differ diff --git a/app/src/main/res/drawable-mdpi/alerts_and_states_warning.png b/app/src/main/res/drawable-mdpi/alerts_and_states_warning.png new file mode 100644 index 00000000..f620ca52 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/alerts_and_states_warning.png differ diff --git a/app/src/main/res/drawable-mdpi/calendar.png b/app/src/main/res/drawable-mdpi/calendar.png new file mode 100644 index 00000000..1bb6a071 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/calendar.png differ diff --git a/app/src/main/res/drawable-mdpi/extra_actions_about.png b/app/src/main/res/drawable-mdpi/extra_actions_about.png new file mode 100644 index 00000000..25d68263 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/extra_actions_about.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_new_account.png b/app/src/main/res/drawable-mdpi/ic_action_new_account.png new file mode 100644 index 00000000..10e17d08 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_new_account.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_new_event.png b/app/src/main/res/drawable-mdpi/ic_action_new_event.png new file mode 100644 index 00000000..e8b7e41a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_new_event.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..d4520816 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_read_only.png b/app/src/main/res/drawable-mdpi/ic_read_only.png new file mode 100644 index 00000000..b2434f76 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_read_only.png differ diff --git a/app/src/main/res/drawable-mdpi/navigation_accept.png b/app/src/main/res/drawable-mdpi/navigation_accept.png new file mode 100644 index 00000000..cf5fab3a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/navigation_accept.png differ diff --git a/app/src/main/res/drawable-mdpi/navigation_forward.png b/app/src/main/res/drawable-mdpi/navigation_forward.png new file mode 100644 index 00000000..75700a7f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/navigation_forward.png differ diff --git a/app/src/main/res/drawable-mdpi/show_sync_settings.png b/app/src/main/res/drawable-mdpi/show_sync_settings.png new file mode 100644 index 00000000..9e7a7756 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/show_sync_settings.png differ diff --git a/app/src/main/res/drawable-mdpi/view_website.png b/app/src/main/res/drawable-mdpi/view_website.png new file mode 100644 index 00000000..7a27476b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/view_website.png differ diff --git a/app/src/main/res/drawable-xhdpi/addressbook.png b/app/src/main/res/drawable-xhdpi/addressbook.png new file mode 100644 index 00000000..3090e596 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/addressbook.png differ diff --git a/app/src/main/res/drawable-xhdpi/alerts_and_states_warning.png b/app/src/main/res/drawable-xhdpi/alerts_and_states_warning.png new file mode 100644 index 00000000..126fc78e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/alerts_and_states_warning.png differ diff --git a/app/src/main/res/drawable-xhdpi/calendar.png b/app/src/main/res/drawable-xhdpi/calendar.png new file mode 100644 index 00000000..5fd726a7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/calendar.png differ diff --git a/app/src/main/res/drawable-xhdpi/extra_actions_about.png b/app/src/main/res/drawable-xhdpi/extra_actions_about.png new file mode 100644 index 00000000..69cacb79 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/extra_actions_about.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_new_account.png b/app/src/main/res/drawable-xhdpi/ic_action_new_account.png new file mode 100644 index 00000000..addee18c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_new_account.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_new_event.png b/app/src/main/res/drawable-xhdpi/ic_action_new_event.png new file mode 100644 index 00000000..f2435cb3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_new_event.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..e4b9000a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_read_only.png b/app/src/main/res/drawable-xhdpi/ic_read_only.png new file mode 100644 index 00000000..43d96617 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_read_only.png differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept.png b/app/src/main/res/drawable-xhdpi/navigation_accept.png new file mode 100644 index 00000000..b8915716 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/navigation_accept.png differ diff --git a/app/src/main/res/drawable-xhdpi/navigation_forward.png b/app/src/main/res/drawable-xhdpi/navigation_forward.png new file mode 100644 index 00000000..1f60fc63 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/navigation_forward.png differ diff --git a/app/src/main/res/drawable-xhdpi/show_sync_settings.png b/app/src/main/res/drawable-xhdpi/show_sync_settings.png new file mode 100644 index 00000000..64bcc5f7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/show_sync_settings.png differ diff --git a/app/src/main/res/drawable-xhdpi/view_website.png b/app/src/main/res/drawable-xhdpi/view_website.png new file mode 100644 index 00000000..aba86f0b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/view_website.png differ diff --git a/app/src/main/res/drawable-xxhdpi/addressbook.png b/app/src/main/res/drawable-xxhdpi/addressbook.png new file mode 100644 index 00000000..c23e339e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/addressbook.png differ diff --git a/app/src/main/res/drawable-xxhdpi/alerts_and_states_warning.png b/app/src/main/res/drawable-xxhdpi/alerts_and_states_warning.png new file mode 100644 index 00000000..c858e047 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/alerts_and_states_warning.png differ diff --git a/app/src/main/res/drawable-xxhdpi/calendar.png b/app/src/main/res/drawable-xxhdpi/calendar.png new file mode 100644 index 00000000..e604ef5d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/calendar.png differ diff --git a/app/src/main/res/drawable-xxhdpi/extra_actions_about.png b/app/src/main/res/drawable-xxhdpi/extra_actions_about.png new file mode 100644 index 00000000..c01120fd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/extra_actions_about.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_new_account.png b/app/src/main/res/drawable-xxhdpi/ic_action_new_account.png new file mode 100644 index 00000000..53c08a8c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_new_account.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_new_event.png b/app/src/main/res/drawable-xxhdpi/ic_action_new_event.png new file mode 100644 index 00000000..ebd24107 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_new_event.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..126ae37e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_read_only.png b/app/src/main/res/drawable-xxhdpi/ic_read_only.png new file mode 100644 index 00000000..6ce79f86 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_read_only.png differ diff --git a/app/src/main/res/drawable-xxhdpi/navigation_accept.png b/app/src/main/res/drawable-xxhdpi/navigation_accept.png new file mode 100644 index 00000000..6fda89ec Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/navigation_accept.png differ diff --git a/app/src/main/res/drawable-xxhdpi/navigation_forward.png b/app/src/main/res/drawable-xxhdpi/navigation_forward.png new file mode 100644 index 00000000..a4e6f382 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/navigation_forward.png differ diff --git a/app/src/main/res/drawable-xxhdpi/show_sync_settings.png b/app/src/main/res/drawable-xxhdpi/show_sync_settings.png new file mode 100644 index 00000000..209f8917 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/show_sync_settings.png differ diff --git a/app/src/main/res/drawable-xxhdpi/view_website.png b/app/src/main/res/drawable-xxhdpi/view_website.png new file mode 100644 index 00000000..05de707e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/view_website.png differ diff --git a/app/src/main/res/layout/account_details.xml b/app/src/main/res/layout/account_details.xml new file mode 100644 index 00000000..40790aba --- /dev/null +++ b/app/src/main/res/layout/account_details.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..1337c4e4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/add_account.xml b/app/src/main/res/layout/add_account.xml new file mode 100644 index 00000000..5bf370dd --- /dev/null +++ b/app/src/main/res/layout/add_account.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/address_books_heading.xml b/app/src/main/res/layout/address_books_heading.xml new file mode 100644 index 00000000..f97ecb90 --- /dev/null +++ b/app/src/main/res/layout/address_books_heading.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/calendars_heading.xml b/app/src/main/res/layout/calendars_heading.xml new file mode 100644 index 00000000..770eb099 --- /dev/null +++ b/app/src/main/res/layout/calendars_heading.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/login_email.xml b/app/src/main/res/layout/login_email.xml new file mode 100644 index 00000000..8e5a0a78 --- /dev/null +++ b/app/src/main/res/layout/login_email.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/login_type.xml b/app/src/main/res/layout/login_type.xml new file mode 100644 index 00000000..8ce0277e --- /dev/null +++ b/app/src/main/res/layout/login_type.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/login_url.xml b/app/src/main/res/layout/login_url.xml new file mode 100644 index 00000000..a712b08f --- /dev/null +++ b/app/src/main/res/layout/login_url.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/query_server.xml b/app/src/main/res/layout/query_server.xml new file mode 100644 index 00000000..5e3f83eb --- /dev/null +++ b/app/src/main/res/layout/query_server.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/select_collections_header.xml b/app/src/main/res/layout/select_collections_header.xml new file mode 100644 index 00000000..521dc124 --- /dev/null +++ b/app/src/main/res/layout/select_collections_header.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/menu/account_details.xml b/app/src/main/res/menu/account_details.xml new file mode 100644 index 00000000..e3bfc0b9 --- /dev/null +++ b/app/src/main/res/menu/account_details.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/menu/add_account.xml b/app/src/main/res/menu/add_account.xml new file mode 100644 index 00000000..6892f08d --- /dev/null +++ b/app/src/main/res/menu/add_account.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/menu/debug_settings.xml b/app/src/main/res/menu/debug_settings.xml new file mode 100644 index 00000000..40f8ba75 --- /dev/null +++ b/app/src/main/res/menu/debug_settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/menu/main_activity.xml b/app/src/main/res/menu/main_activity.xml new file mode 100644 index 00000000..132a9981 --- /dev/null +++ b/app/src/main/res/menu/main_activity.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/menu/only_next.xml b/app/src/main/res/menu/only_next.xml new file mode 100644 index 00000000..bd075ce5 --- /dev/null +++ b/app/src/main/res/menu/only_next.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 00000000..64132a6c --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,142 @@ + + + + + DAVdroid + + Lloc web de DAVdroid + Següent + Ajuda + + Error HTTP: %s + Falten capacitats: %s + Error E/S: %s + URI invàlida: %s + + + Gestioneu els comptes a sincronitzar + Gràcies per comprar DAVDroid a travès de Google Play i així donar suport al projecte. De totes formes, hi ha dos incidències amb el Google Play:

+ +

1. Les comptes poden desaparèixer desprès de reiniciar

+

Podeu trobar-vos amb el problema de que totes les vostres comptes de DAVdroid (contactes i events inclosos) han desaparegut + desprès de reiniciar el dispositiu. La raó és un error de Android + que causa que totes les comptes de les aplicacións de pagament s\'eliminen al iniciar perquè els fitxers (encriptats) APK es + carreguen desprès de revisar si existeixen comptes orfanes.

+

Usuaris afectats:
+ * Tots els usuaris de Android 4.1 que han obtingut el DAVdroid del Play Store;
+ * El usuaris de Android 4.2 que han obtingut el DAVdroid del Play Store només en alguns dispositius (per exemple, la majoria de dispositius Samsung)

+ +

2. Els comptes poden desaparèixer desprès d\'actualitar el DAVdroid

+

Podeu trobar-vos amb el problema de que totes les vostres comptes de DAVdroid (contactes i events inclosos) han desaparegut quan el Play Store + actualitza el DAVdroid. La raó és unaltre error de Android + que causa que les comptes del les aplicacións de pagament s\'esborrin al actualitzar per alguna raó similar.

+

Usuaris afectats: alguns de Android 4.4.2 que han obtingut el DAVdroid desde el Play Store (es coneix per dispositius Nexus i Moto G)

+ +

Si esteu afectats per algun d\'aquests errors, sisuplau instaleu el + DAVdroid JB Workaround.

+ ]]>
+ Benvingut a DAVdroid/%s! + +

DAVdroid es un adaptador de Android 4+ per la sincronització de CalDAV/CardDAV. Per a utilitzar-lo afegieu una compte de DAVdroid + pel vostre servidor de CalDAV/CardDAV i els vostres contactes/esdeveiments es sincronitzaràn de forma bidireccional.

+ +

Per mes informació, podeu visitar la pàgina web de DAVdroid. + També hi ha una guia de configuració. El DAVdroid respecta + la teva privacitat, feu una ullada a la nostra politica de privacitat.

+ +

Si feu servir CyanogenMod, "Privacy Guard" ha d\'estar deshabilitat pel DAVdroid. Si no es així, el DAVdroid no pot accedir ni sincronitzar + els vostres contactes i events.

+ +

En cas de problemes, sisplau llegiu el FAQ primer. + Si trobeu una errada que està clarament relacionada amb el DAVdroid, afegiu-la directament a + Github en comptes de contactar-nos directament o de donar una + valoració pobra de l\'aplicació.

+ +

Codi Obert

+

DAVdroid està disenyat per a ser un proejcte de codi obert desde bon principi. Sempres es possible compilar l\'aplicació per la vostra part + i utilitzar-la gratuitament sense cap obligació. El codi font es troba + app yourself and use it for free without any obligations. The source code is + disponible a Github, i també podeu + descarregar l\'aplicació al F-droid.

+ +

De totes formes, es va fer molta feina per crear aquesta applicació, així que vam decidir ficar-la a les tendes comercials per un petit import. + Si voleu suportar aquest project, siusplau considereu fer una donació a DAVdroid + o comprar-lo.

+ +

Llicència

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). Tots els drets reservats. + Aquest programa i tots els materials que l\'acompanyen estan disponibles sota els termes de la GNU Public License v3.0 que acompanya + aquesta distribució i està disponible a http://www.gnu.org/licenses/gpl.html. Respecte al Google Play, Samsung + Store, AndroidPit App Center o Amazon Appstore que requereixen altres condicions, els termes respectius apliquen per les versions + que s\'han descàrregat a travès d\'aquests serveis.

+ +

La traducció alemana es realitzada pels mateixos autors. Les traduccions a altres idiomes han estat contribucións de varies persones que es mencionen en el seu idioma específic. +

+

Traducció: + Català: Sergi Almacellas Abellana +

+ +

Llibraries de tercers utilitzades

+

+ * iCal4j (New BSD License)
+ * ez-vcard (New BSD License)
+ * Simple XML Serialization (Apache License, Version 2.0)
+ * Project Lombok (MIT License)

+ * dnsjavaBSD License

+ ]]>
+ + + Entra amb una adreça de correu electrònic + Els detalls del servei es detectaran automàticament amb el nom del servidor. Per exemple: elmeucompte@icloud.com + Entra amb una URL i un nom d\'usuari + Els detalls del servei es detectaran de forma automàtica amb al URL inicial i el nom d\'usuari. Normalment utilitzat per servers allotjats en servidors propis. + + Si us plau, introduïu la vostra adreça de correu electrònic. El seu nom de domini s\'utiltizara per auto-detectar la configuració dels serveis. + Correu electrònic: + + http:// + https:// + + + "Si no feu servir cap encriptació (HTTPS), altres usuaris poden interceptar fàcilment les vostres credencials, contactes i events." + Nom d\'usuari: + URL Arrel (les coleccions es detecten automàticament): + Autentificació preferent (recomanat però incompatible amb l\'autentificació Digest) + Contrasenya: + DAVdroid: Seleccioneu col·leccions + Ni CalDAV ni CardDAV estan disponibles a aquesta ubicació. + + Afegir compte + Contactant servidor. Espereu sisuplau. + Quines col·leccions s\'han de sincronitzar? + Llibretes de contactes + Llibreta de contactes + Calendaris + Calendari + Seleccioneu com a molt una llibreta de contactes (Polseu de nou per deseleccionar): + Seleccioneu els vostres calendaris: + + Detalls del compte + Nom del compte: + El meu compte CalDAV/CardDAV + Correu electrònic: + "ORGANITZADOR dels teus events. Es requereix si feu servir la informació dels assitents" + "Podeu fer servir la vostra addreça de correu electrònic com a nom de la compta ja que el Android utiltizar el nom de la compta com a camp ORGANITZADOR pels events que vosaltres creeu. No podeu tenir dos comptes amb el mateix nom. + només-lectura + + + Configuració general + Configuració deputarció + Desactiva compressió HTTP + La compressió HTTP està desactivada (mode depuració) + La compressió HTTP s\'utilitza quan es possible. + Registra el tràfic de xarxa + Tot el tràfic de xarxa es reigstra (mode depuració) + No es registra el tràfic de xarxa + Informa d\'un error + + +
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 00000000..c74d42dc --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,141 @@ + + + + + DAVdroid + + Webová stránka DAVdroid + Další + Pomoc + + HTTP chyba: %s + Chybějící možnosti: %s + I/O chyba: %s + Neplatné URI: %s + + + Spravovat synchronizované účty + + Thank you for buying DAVdroid via Google Play and thus supporting the project. Unfortunately, there are two issues with Google Play:

+ +

1. Accounts may be gone after a reboot

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) are gone + after rebooting your device. The reason is a bug in Android + that causes accounts of paid apps to be removed on start-up because the (encrypted) APK files are + loaded after checking for orphaned accounts.

+

Affected users:
+ * all Android 4.1 users who have got DAVdroid from Play Store;
+ * Android 4.2 users who have got DAVdroid from Play Store only with certain devices (for instance, most Samsung devices)

+ +

2. Accounts may be gone after upgrading DAVdroid

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) when Play Store + updates DAVdroid. The reason is another bug in Android + that causes accounts of paid apps to be removed when upgrading for a similar reason.

+

Affected users: some Android 4.4.2 users who have got DAVdroid from Play Store (known for Nexus devices and Moto G)

+ +

If you\'re affected by one of these bugs, please install the + DAVdroid JB Workaround.

+ ]]>
+ Vítejte do aplikace DAVdroid/%s! + +

DAVdroid je aplikace pro Android 4+ na synchronizaci CalDAV/CardDAV účtů. Stačí přidat účet DAVdroid + pro CalDAV/CardDAV server a vaše kontakty/události budou synchronizovány oběma směry.

+ +

Pro více informací se podívejte na domovskou stránku aplikace DAVdroid. + Najdete tam i návod pro nastavení. DAVdroid respektuje + vaše soukromí, přečtěte si naši Privacy Policy.

+ +

If you use CyanogenMod, "Privacy Guard" must be disabled for DAVdroid. Otherwise, DAVdroid is not allowed to access + and synchronize your contacts and events.

+ +

In case of problems, please read the FAQ first. + If you encounter a bug that is clearly related to DAVdroid, enter it on + Github issues instead of contacting us directly or giving a poor + rating for the app.

+ +

Open-source

+

DAVdroid is designed to be an open-source project from the very first beginning. It is always possible to compile the + app yourself and use it for free without any obligations. The source code is + available on Github, and you can + download the app on F-droid.

+ +

However, it was much work to create this app, so we have decided to put it into the commercial stores for a small fee. + If you want to support this project, please consider donating to DAVdroid + or purchasing it.

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Překlad. + Čeština: Jaroslav Lichtblau +

+ +

Použité knihovny třetích stran

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Přihlášení s emailovou adresou + Detaily účtu budou automaticky detekovány podle doménového jména. Příkald: mujucet@icloud.com + Přihlášení s URL a uživatelským jménem + Detaily účtu budou automaticky rozpoznány podle URL a uživatelského jména. Nejčastější volba pro vlastní servery. + + Vložte prosím svou emailovou adresu. Její doménová adresa bude použita pro automatické zjištění nastavení služby. + Email: + + + http:// + https:// + + "Pokud nepoužijete šifrované připojení (HTTPS), ostatní budou moci lehce získat vaše přihlašovací údaje, kontakty a události." + Uživatelské jméno: + Kořenová URL (sbírky budou detekovány automaticky): + Preemptivní ověření (doporučeno, ale není kompatibilní s Digest ověřením) + + Heslo: + + DAVdroid: Vybrat sbírky + V tomto umístění není CalDAV-/CardDAV služba dostupná. + Přidat účet + Probíhá komunikace se serverem. Chvilku strpení… + Které sbírky mají být synchronizovány? + Adresáře + Adresář + Kalendáře + Kalendář + Vybrat alespoň jeden adresář (opakovaně tapnout pro odebrání): + Vybrat své kalendáře: + + Detaily účtu + Jméno účtu: + Můj CalDAV/CardDAV účet + Emailová adresa: + "ORGANIZÁTOR událostí; vyžadováno pokud budete přidávat účastníky" + "Použijte svou emailovou adresu jako jméno účtu. Android bude používat tuto hodnotu jako jméno ORGANIZÁTORA událostí které vytvoříte. Nelze mít dva účty se stejným jménem. + pouze pro čtení + + + Obecná nastavení + Ladící nastavení + Vypnout HTTP kompresi + HTTP komprese je vypnuta (ladící mód) + HTTP komprese je použita všude kde možno + Logovat síťový provoz + Veškerý síťový provoz je detailně logován (ladící mód) + Síťový provoz není logován + Nahlásit problém + +
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..d77ccd47 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,138 @@ + + + + + DAVdroid-Website + Weiter + Hilfe + + HTTP-Fehler: %s + Fehlende Server-Unterstützung: %s + E/A-Fehler: %s + URI ungültig: %s + + + Sync-Konten anzeigen + + Vielen Dank, dass Sie DAVdroid im Play Store erworben haben und das Projekt dadurch unterstützen. + Leider gibt es derzeit zwei Probleme, die vom Play Store verursacht werden:

+ +

1. DAVdroid-Accounts verschwinden nach einem Neustart

+

Möglicherweise verschwinden alle Ihre DAVdroid-Accounts samt Kontakten und Terminen nach einem Neustart + des Geräts. Die Ursache ist ein Fehler in Android, + der zur irrtümlichen Entfernung von Accounts von Bezahlapps bei einem Neustart führt, da die Prüfung auf verwaiste + und damit zu löschende Accounts schon *vor* dem Entschlüsseln der App erfolgt.

+

Betroffene Benutzer*Innen:
+ * alle mit Android 4.1, die DAVdroid über Play Store bezogen haben;
+ * einige mit Android 4.2, die DAVdroid über Play Store bezogen und bestimmte Geräte haben (zB die meisten Samsung-Geräte)

+ +

2. DAVdroid-Accounts verschwinden nach einer DAVdroid-Aktualisierung

+

Möglicherweise verschwinden alle Ihre DAVdroid-Accounts samt Kontakten und Terminen während eines + DAVdroid-Updates, das von Play Store durchgeführt wird. Die Ursache ist ein + anderer Fehler in Android, + der zur irrtümlichen Entfernung von Accounts von Bezahlapps bei der Aktualisierung dieser Apps führt.

+

Betroffene Benutzer*Innen:
+ einige mit Android 4.4.2, die DAVdroid über Play Store bezogen haben (bekannt sind Nexus-Geräte und Moto G)

+ +

Sollten Sie von einem dieser Fehler betroffen sein, installieren + Sie den DAVdroid JB Workaround.

+ ]]>
+ Willkommen bei DAVdroid/%s! + +

DAVdroid ist ein Android 4+-CalDAV/CardDAV-Sync-Adapter. Um ihn zu verwenden, müssen Sie ein DAVdroid-Konto + für Ihren Server hinzufügen. Die Kontakte/Termine werden dann automatisch in beide Richtungen synchronisiert.

+ +

Wenn Sie CyanogenMod benutzen, muss die "Datenschutz"-Funktion für DAVdroid deaktiviert sein, da DAVdroid sonst + keinen Zugriff auf die Kontakte und Kalendar hat und auch nicht synchronisieren kann.

+ +

Weitere Informationen erhalten Sie auf der DAVdroid-Homepage. + Dort finden Sie auch eine Anleitung zum Einrichten. + DAVdroid respektiert Ihre Privatsphäre (siehe Datenschutzrichtlinie).

+ +

Bei Problemen lesen Sie bitte die häufig gestellten Fragen. + Im Falle eines Fehlers, der eindeutig durch DAVdroid verursacht wird, berichten Sie diesen wenn möglich auf + Github Issues, anstatt uns direkt zu kontaktieren oder die App + schlecht zu bewerten.

+ +

Open-Source

+

DAVdroid ist von Anfang an als Open-Source-Projekt ausgelegt. Der Quellcode kann jederzeit selbst kompiliert und + die App unter den Bedingungen der GPLv3 verwendet werden. Der Quellcode ist + auf Github verfügbar, die App kann auch + über F-Droid bezogen werden.

+ +

Es ist jedoch viel Arbeit, die App zu entwickeln und besser zu machen. Daher haben wir uns entschlossen, sie + auch gegen eine kleine Gebühr in die kommerziellen App-Stores zu stellen. Wenn Sie das Projekt unterstützen wollen, können Sie + für DAVdroid spenden oder die App kaufen.

+ +

Lizenz

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (bitfire web engineering), alle Rechte + vorbehalten. Dieses Programm ist freie Software. Sie können es unter den Bedingungen der GNU + General Public License Version 3, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder modifizieren. + Sofern Google Play oder Samsung Store andere Bedingungen benötigen, gelten für über den jeweiligen Markt heruntergeladene + Apps diese Bedingungen.

+ +

Die Übersetzung auf Deutsch wird von den Autoren zur Verfügung gestellt. Übersetzungen für andere Sprachen + wurden von verschiedenen Leuten beigetragen, die in der jeweiligen Sprachversion erwähnt sind.

+ +

Verwendete Bibliotheken

+

+ * Apache HttpClient (mittels httpclientandroidlib) – Apache License, Version 2.0
+ * iCal4jNew BSD License
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Mit Email-Adresse anmelden + Domänenname wird verwendet, um die Servereinstellungen herauszufinden. Beispiel: myaccount@icloud.com + Mit URL und Benutzername anmelden + Basis-URL und Benutzername werden verwendet, um die Servereinstellungen herauszufinden; z.B. bei einem eigenen Server. + + Geben Sie Ihre Email-Adresse ein. Der Domänenname wird verwendet, um die Servereinstellungen herauszufinden. + Email: + + Ohne Verschlüsselung (HTTPS) können Ihre Zugangsdaten, Kontakte und Termine leicht abgefangen werden. + Benutzername: + Basis-URL (Ordner werden automatisch gefunden): + Präemptive Authentifizierung (empfohlen, aber nicht kompatibel mit Digest-Auth.) + + Passwort: + + DAVdroid: Ordner auswählen + An dieser Adresse konnte kein CalDAV- oder CardDAV-Dienst gefunden werden. + Konto hinzufügen + Daten werden vom Server abgefragt. Bitte warten… + Welche Ordner sollen synchronisiert werden? + Adressbücher + Adressbuch + Kalender + Kalender + Ein oder kein Adressbuch auswählen (nochmal berühren, um abzuwählen): + Kalender zur Synchronisation auswählen: + + Konto-Details + Kontoname: + Mein CalDAV/CardDAV-Konto + Email-Adresse: + "ORGANIZER der von Ihnen angelegten Termine; notwendig für Teilnehmer-Info" + "Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen. + schreibgeschützt + + + Hilfe zu DAVdroid + + Allgemeine Einstellungen + Einstellungen zur Fehlersuche + HTTP-Komprimierung deaktivieren + HTTP-Komprimierung ist deaktiviert (zur Fehlersuche) + HTTP-Komprimierung wird verwendet, falls möglich + Netzwerkverkehr aufzeichnen + Der gesamte Netzwerkverkehr wird in den Android-Logs mitgeschrieben (zur Fehlersuche) + Netzwerkverkehr wird nicht aufgezeichnet + Problem berichten + +
\ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..da7c99c6 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,86 @@ + + + + DAVdroid + Ajustes + + + http:// + https:// + + + "Si no usas encriptación (HTTPS), otras personas pueden interceptar fácilmente tus datos de ingreso, contactos y eventos." + Nombre de usuario: + Contraseña: + URL raiz (colecciones autodetectadas): + Siguiente + Ayuda + URL base no válida: + DAVdroid: Selecciona las colecciones + I/O error: %s + URI no válida: %s + Se han perdido capacidades: %s + Ni CalDAV ni CardDAV están disponibles + Añadir cuenta + Consultando el servidor. Espera, por favor… + error HTTP: %s + Qué colecciones quieres sincronizar? + Agendas + Calendarios + Seleciona una agenda (pulsar de nuevo para desmarcar): + Selecciona tus calendarios: + Autentificación preferente (recomendado, pero incompatible con la autentificación de Digest) + + Ayuda de DAVdroid + Administrar las cuentas sincronizadas + Sitio web de DAVdroid + ¡Bienvenido a DAVdroid/%s! + +

DAVdroid es un adaptador de sincronización entre Android 4+ y CalDAV/CardDAV. Para usarlo, simplemente añade una cuenta DAVdroid para tu servidor de CalDAV/CardDAV, así tus contactos y eventos serán sincronizados en ambas direcciones.

+ +

Si usas CyanogenMod, debes desactivar "Privacy Guard" para DAVdroid. De otro modo, DAVdroid no tendrá autorización para acceder y sincronizar tus contactos y eventos.

+ +

Para más información, por favor lee la página de DAVdroid. + También hay una Guía de configuración. DAVdroid respeta tu privacidad, lee nuestra Politica de Privacidad.

+ +

Si tienes problemas, por favor lee las FAQ primero. + Si te encuentras un bug que esté claramente relacionado con DAVdroid, entra en + Github issues en lugar de contactar con nosotros directamente o de darle una mala votación a la aplicación.

+ +

Código abierto

+

DAVdroid ha sido diseñado como un proyecto de código abierto desde sus inicios. Siempre es posible compilar la app por ti mismo y usarla de forma gratuita sin ninguna obligación. El código fuente está disponible en Github, y puedes descargar la app desde F-droid.

+ +

Sin embargo, hay mucho trabajo detrás de la creación de esta aplicación, así que hemos decidido ponerla en las tiendas (Google Play, + Samsung Store) a cambio de un pequeño pago. + Si quieres apoyar este proyecto, por favor piensa en hacer una donación a DAVdroid + o en comprar la app.

+ +

Licencia

+

Copyright (c) 2013 – 2014 Richard Hirner (bitfire web engineering). Todos los derechos reservados. + Este programa y los materiales que la acompañan está disponible bajo las condiciones de la GNU Public License v3.0 que acompaña a esta distribución, y está a tu disposición en http://www.gnu.org/licenses/gpl.html. En lo relativo a Google Play or Samsung requieren otras condiciones, éstas han sido descargadas a través de estos servicios.

+ +

Translations. + Catalanian: @pokoli, + Chinese (simplified): @phy25, + Czech: Jaroslav Lichtblau, + Serbian: @pejakm, + Spanish: @xphnx +

+ +

Librerías third-party usadas

+

+ * iCal4j (Licencia New BSD)
+ * ez-vcard (Licencia New BSD)
+ * Simple XML Serialization (Licencia Apache Version 2.0)
+ * Project Lombok (Licencia MIT)

+ ]]>
+ Detalles de la cuenta + Nombre de la cuenta: + Mi cuenta CalDAV/CardDAV + Dirección de correo: + "ORGANIZADOR de tus eventos; se necesita si se usa información de los asistentes" + "Usa tu dirección de correo electrónico como nombre de cuenta porque Android usará el nombre de cuenta como campo de ORGANIZADOR para los eventos que crees. No puedes tener dos cuentas con el mismo nombre. + +
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..c6a8aa2f --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,93 @@ + + + + DAVdroid + Paramètres + + + http:// + https:// + + + Si vous n\'utilisez pas de chiffrement(HTTPS), d\'autres personnes peuvent facilement intercepter vos informations de connexion, contacts et événements. + Utilisateur: + Mot de passe: + URL racine(les collections seront autodétectées): + Suivant + Aide + Racine de l\'URL incorrecte: + DAVdroid: Sélectionnez les collections + Erreur I/O: %s + URI incorrecte: %s + Capacités manquantes: %s + Aucun CalDAV ou CardDAV disponible + Ajouter un compte + Interroge le serveur. Patientez svp. + Erreur HTTP: %s + Quelles collections doivent être synchronisées ? + Carnets d\'adresses + Agendas + Choisissez un carnet d\'adresses(toucher à nouveau pour désélectionner): + Choisissez vos agendas: + Authentification préventive(recommandé, mais incompatible avec l\'authentification Digest) + + Aide de DAVdroid + Gérer les comptes synchronisés + Site Web de DAVdroid + Bienvenue dans DAVdroid/%s! + +

DAVdroid est un connecteur de synhronisation entre Android 4+ et CalDAV/CardDAV. Pour l\'utiliser, il suffit d\'ajouter un compte DAVdroid + pour votre serveur CalDAV/CardDAV, et vos contacts/évènements seront synchronisés dans les deux sens.

+ +

Si vous utilisez CyanogenMod, "Privacy Guard" doit être désactivé pour DAVdroid. Sinon DAVdroid ne sera pas en mesure d\'accéder + et synchroniser vos contacts et évènements

+ +

Pour plus d\'informations, visitez la page d\'accueil de DAVdroid. + Il y a Un guide d\'installation également. DAVdroid respecte + votre vie privée, see our Politique de confidentialité.

+ +

En cas de problème, merci de lire la FAQ en premier lieu. + Si vous rencontrer un bug qui est clairement lié à DAVdroid, svp saisissez le sur + Github issues au lieu de nous contacter directement ou d\'attribuer une mauvaise appréciation à l\'application.

+ +

Open-source

+

DAVdroid est conçu depuis le début comme étant open-source. Il est toujours possible de le compiler vous-même l\'application + et l\'utiliser gratuitement sans autre obligation. Le code source est + disponible sur Github, et vous pouvez + télécharger l\'application sur F-droid.

+ +

Néanmoins étant donné que créer cette application nécessite du travail, nous avons décidé de la mettre sur les Stores (Google Play, + Samsung Store) pour un faible coût. + Si vous voulez aider ce projet faites un don à DAVdroid ou achetez le

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). All rights reserved. + Ce programme et les documents qui l\'accompagnent sont mis à disposition sous les termes de la Licence Public GNU v3.0 qui + accompagne cette distribution, et est disponible à http://www.gnu.org/licenses/gpl.html. En ce qui concerne Google Play ou Samsung Store, les conditions respectives s\'appliquent pour les versions qui sont téléchargées via ces services.

+ +

Translations. + Catalanian: @pokoli, + Chinese (simplified): @phy25, + Czech: Jaroslav Lichtblau, + Serbian: @pejakm, + Spanish: @xphnx +

+ +

Bibliothèques tiers

+

+ * iCal4j (New BSD License)
+ * ez-vcard (New BSD License)
+ * Simple XML Serialization (Apache License, Version 2.0)
+ * Project Lombok (MIT License)

+ ]]>
+ Détails du compte + Nom du compte: + Mon compte CalDAV/CardDAV + Adresse Email: + ORGANISATEUR de vos événements; nécessaire pour l\'information des participants + Utilisez votre adresse email en tant que nom de compte car Android utilise ce nom pour le champ ORGANISATEUR des évènements que vous créez." + Vous ne pouvez pas avoir deux comptes du même nom. + +
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 00000000..e8d159dd --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,147 @@ + + + + + DAVdroid + + DAVdroid web oldal + Tovább + Súgó + + HTTP hiba: %s + Hiányzó szolgáltatás: %s + I/O hiba: %s + Érvénytelen URI: %s + + + Szinkronizációs fiókok kezelése + + Köszönjük, hogy megvásárolta a DAVdroid alkalmazást a Google Play áruházban, támogatva ezzel a projektet. Sajnos, + a Google Play áruházzal két probléma is van:

+

1. A fiókok az eszköz újraindítása után eltűnhetnek

+

Előfordulhat, hogy az eszköz újraindítása után az összes DAVdroid fiók (beleértve hozzájuk tartozó + névjegyeket és eseményeket) eltűnik. Ezt egy + Android szoftverhiba okozza, melynek + következtében a nem ingyenes alkalmazásokhoz tartozó fiókok az eszköz indításakor törlődnek. A hiba hátterében az áll, + hogy a (kódolt) APK fájlok a létrehozó nélküli ("megárvult") fiókok törlése után töltődnek be.

+

Érintett felhasználók:
+ * minden Android 4.1 felhasználó, aki a DAVdroid alkalmazást a Google Play áruházból töltötte le;
+ * egyes Android 4.2 felhasználók, akik a DAVdroid alkalmazást a Google Play áruházból töltötték le, a használt eszköz + típusától függően (például egyes Samsung eszközök felhasználói)

+

2. A fiókok a DAVdroid frissítése után eltűnhetnek

+

Előfordulhat, hogy az összes DAVdroid fiók (beleértve a hozzájuk tartozó névjegyeket és eseményeket) + eltűnik, miután a Google Play áruház frissítette a DAVdroid alkalmazást. Ezt szintén egy, az előzőhöz hasonló, + Android szoftverhiba okozza. +

Érintett felhasználók:
+ * egyes Android 4.4.2 felhasználók, akik a DAVdroid alkalmazást a Google Play áruházból töltötték le, a használt eszköz + típusától függően (például Nexus és Moto G eszközök felhasználói)

+

A probléma a JB DAVdroid gyorsjavítás + telepítésével kiküszöbölhető.

+ ]]>
+ Üdvözöljük a DAVdroid/%s felhasználók között! +

DAVdroid egy Android 4+ CalDAV/CardDAV szinkronizációs adapter. Használatához hozzon létre egy DAVdroid fiókot a használni + kívánt CalDAV/CardDav szerverhez. Ezt követően a névjegyek és események szinkronizálva lesznek, mindkét irányban.

+

További információkat a DAVdroid + honlap tartalmaz. A beállításokkal kapcsolatban a + beállítások oldalon + találhat információkat. A DAVdroid tiszteletben tartja adatait bizalmasságát, részleteket az + adatkezelési nyilatkozat + tartalmaz.

+

Ha Ön CyanogenMod felhasználó, vonja ki a DAVdroidot a "Privacy Guard" hatálya alól, különben a DAVdroid nem fog tudni + hozzáférni a névjegyekhez és eseményekhez.

+

Probléma esetén olvassa el a + GYIK-et. Ha + egyértelműen a DAVdroidhoz köthető hibába ütközik, ahelyett, hogy közvetlenül keresne minket, vagy lepontozná az alkalmazást, + inkább a Problémák oldalon jelentse be.

+

Nyílt forráskód

+

A DAVdroid kezdettől fogva nyílt forráskódú projekt. Bármikor lehetősége van az alkalmazást lefordítani és használni, + mindenféle kötelezettség nélkül. A forráskód elérhető a Githubon, a + lefordított alkalmazás pedig az + F-droidon.

+

Mindemellett, az alkalmazás kifejlesztése sok munkát igényelt, ezért úgy döntöttünk, hogy az alkalmazást más áruházakban + is elérhetővé tesszük, egy szerény díj ellenében. Ha szeretné támogatni ezt a projektet, kérjük, fontolja meg az alkalmazás + megvásárlását vagy közvetlen + támogatását.

+

Licenc

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). Minden jog fenntartva. + Ez a program és a kapcsolódó anyagok a GNU Public License v3.0 hatálya alatt állnak, amely részét képezi a jelen csomagnak, és + amely elérhető + http://www.gnu.org/licenses/gpl.html oldalon. Amennyiben a Google Play, Samsung + Store, AndroidPit App Center vagy Amazon Appstore áruház eltérő feltételeket szab, akkor annak az áruháznak a szabályai + irányadóak, ahonnan az alkalmazást letöltötte.

+

Fordítások. + cseh: Jaroslav Lichtblau, + katalán: @pokoli, + kínai (egyszerűsített): @phy25, + magyar: Gábor J.Tóth, + szerb: @pejakm, + spanyol: @xphnx. +

+

Harmadik felek által fejlesztett programcsomagok

+

+ * Apache HttpClient ( + httpclientandroidlib változat) – Apache License, Version 2.0
+ * iCal4j + New BSD License
+ * ez-vcard + New BSD License
+ * Simple XML Serialization + Apache License, Version 2.0
+ * Project LombokMIT License

+ ]]>
+ + + Bejelentkezés email cím segítségével + A szolgáltatás részleteinek automatikus detektálása a tartománynév alapján történik. Példa: myaccount@icloud.com + Bejelentkezés URL és felhasználónév segítségével + A szolgáltatás részleteinek automatikus detektálása a kiinduló URL és a felhasználónév alapján történik. Elsősorban privát szolgáltatásoknál ajánlott. + + Kérjük, adja meg email címét. A szolgáltatás részleteinek automatikus detektálása ennek + tartománynév-része alapján fog történni. + Email: + + + http:// + https:// + + "Titkosítás (HTTPS) nélkül a bejelentkezési azonosítókat, névjegyeket és eseményeket könnyen megismerhetik mások is." + Felhasználónév: + Fiók URL (a gyűjtemények detektálása automatikus): + Preemptív authentikáció (ajánlott, de Digest authentikációval nem működik) + + Jelszó: + + DAVdroid: Gyűjtemény kiválasztása + Nincs CalDAV-/CardDAV szolgáltatás a megadott helyen. + Fiók hozzáadása + Kapcsolódás a szerverhez. Egy pillanat… + Melyik gyűjtemények legyenek szinkronizálva? + Címjegyzékek + Címjegyzék + Naptárak + Naptár + Egy címjegyzék választható (a kijelölés visszavonása újbóli érintéssel vagy másik tétel kiválasztásával): + Naptárak kiválasztása: + + További beállítások + A fiók neve: + CalDAV/CardDAV fiók + Email cím: + "Szervező (ORGANIZER mező értéke), résztvevők kezelése esetén" + "Használja az email címet fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni. + csak olvasható + + + Általános beállítások + Hibakeresési beállítások + HTTP tömörítés kikapcsolása + HTTP tömörítés kikapcsolva (hibakeresés) + HTTP tömörítés bekapcsolva (ahol csak lehetséges) + Hálózati forgalom naplózása + A teljes hálózati forgalom részletes naplózása (hibakeresés) + A hálózati forgalom naplózása kikapcsolva + Probléma bejelentése + +
\ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 00000000..ebf462b7 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,136 @@ + + + + + ДАВдроид + + ДАВдроид вебсајт + Следеће + Помоћ + + ХТТП грешка: %s + Недостају могућности: %s + У/И грешка: %s + Неисправан УРИ: %s + + + Управљај налозима синхронизације + + Хвала вам што сте купили ДАВдроид преко Google Play сервиса и тиме подржали овај пројекат. Нажалост, постоје два проблема са Google Play сервисом:

+ +

1. Налози могу нестати након поновног покретања

+

Може вам се десити да сви ваши ДАВдроид налози (укључујући контакте и догађаје) нестану након + поновног покретања вашег уређаја. Разлог је грешка у Андроиду + која узрокује уклањање налога плаћених апликација приликом покретања јер се (шифровани) АПК фајлови + учитавају након провере за налоге без апликација.

+

Захваћени корисници:
+ * сви корисници Андроида 4.1 који су инсталирали ДАВдроид са Play Store сервиса;
+ * корисници Андроида 4.2 који су инсталирали ДАВдроид са Play Store сервиса само на неким уређајима (нпр. већина Самсунгових уређаја)

+ +

2. Налози могу да нестану након надоградње ДАВдроида

+

Може вам се десити да сви ваши ДАВдроид налози (укључујући контакте и догађаје) нестану након + надоградње ДАВдроида. Разлог је опет грешка у Андроиду + која узрокује уклањање налога плаћених апликација приликом надоградње из сличног разлога.

+

Захваћени корисници: неки корисници Андроида 4.4.2 који су инсталирали ДАВдроид са Play Store сервиса (Нексус и Мото Г уређаји)

+ +

Ако имате један од наведених проблема, инсталирајте „DAVdroid JB Workaround“.

+ ]]>
+ Добро дошли у ДАВдроид/%s! + +

ДАВдроид (DAVdroid) је Андроид 4+ адаптер синхронизације за КалДАВ/КардДАВ (CalDAV/CardDAV). Да бисте га користили, + само додајте ДАВдроид налог за ваш КалДАВ/КардДАВ сервер и ваши контакти/догађаји ће бити синхронизовани у оба смера.

+ +

За више информација погледајте ДАВдроид вебсајт. + Постоји и водич за поставку. ДАВдроид поштује + вашу приватност, погледајте нашу политику приватности.

+ +

Ако користите Цијаноген Мод, „Privacy Guard“ мора бити онемогућен за ДАВдроид. У супротном, ДАВдроиду неће бити дозвољени + приступ и синхронизација ваших контаката и догађаја.

+ +

У случају проблема, најпре прочитајте најчешћа питања. + Ако вам се деси грешка која је сигурно везана за ДАВдроид, поднесите пријаву на + Гитхабовом пратиоцу уместо да нас контактирате директно или да апликацији дате лошу оцену.

+ +

Отворени код

+

ДАВдроид је испочетка дизајниран као пројекат отвореног кода. Увек је могуће да сами компајлирате + апликацију и да је слободно користите без икаквих обавеза. Изворни код је + доступан на Гитхабу, а можете и + преузети апликацију са Ф-Дроида.

+ +

Међутим, уложено је много рада у ову апликацију па смо одлучили да је ставимо у комерцијалне продавнице за малу накнаду. + Ако желите да подржите овај пројекат, донирајте ДАВдроиду + или га купите.

+ +

Лиценца

+

Ауторско право © 2013 – 2014 Рики Хирнер (Ricki Hirner), Бернхарт Стокман (Bernhard Stockmann) (Битфајер веб инжињеринг). Сва права задржана. + Овај програм и пратећи материјали су доступни под условима ГНУ-ове Јавне Лиценце в3.0 која је приложена, + и доступна на http://www.gnu.org/licenses/gpl.html. Ако сервиси „Google Play“, + „Samsung Store“, „AndroidPit App Center“ или „Amazon Appstore“ захтевају друге услове, исти важе за издања преузета са ових сервиса.

+ +

Превод на српски: @pejakm. Превод на немачки: аутори. Остале преводе + допринели су људи поменути у преводима одговарајућих језика.

+ +

Коришћене друге библиотеке

+

+ * Апачи ХТТП клијент (httpclientandroidlib издање) – Апачи лиценца, издање 2.0
+ * iCal4jНова БСД лиценца)
+ * ez-vcardНова БСД лиценца
+ * Симпле ИксМЛ серијализацијаАпачи лиценца, издање 2.0
+ * Пројекат ЛомбокМИТ лиценца

+ * dnsjavaБСД лиценца

+ ]]>
+ + + Пријавите се адресом е-поште + Детаљи сервиса ће бити аутоматски откривени по имену домена. Пример: mojnalog@icloud.com + Пријавите се УРЛ-ом и корисничким именом + Детаљи сервиса ће бити аутоматски откривени по почетном УРЛ-у и корисничком имену. Углавном се користи за самохостоване сервисе. + + Унесите вашу адресу е-поште. Име домена ће бити коришћено за аутоматско откривање поставки сервиса. + Е-адреса: + + + http:// + https:// + + "Ако не користите шифровање (ХТТПС), други људи вам лако могу пресрести детаље пријаве, контакте и догађаје." + Корисничко име: + Корени УРЛ (збирке ће бити аутоматски откривене): + Превентивна аутентификација (препоручено, али некомпатибилно са Дигест аутентификацијом) + + Лозинка: + + ДАВдроид: Изаберите збирке + Нема доступног КалДАВ/КардДАВ сервиса на овој локацији. + Додај налог + Шаљем упит серверу. Сачекајте… + Које збирке да синхронизујем? + Адресари + Адресар + Календари + Календар + Изаберите један адресар (додирните поново да поништите избор): + Изаберите ваше календаре: + + Детаљи налога + Име налога: + Мој КалДАВ/КардДАВ налог + Е-адреса: + "ОРГАНИЗАТОР ваших догађаја; потребно ако користите податке о учеснику" + "Користите вашу е-адресу као име налога јер Андроид користи име налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог имена. + само-за-читање + + + Опште поставке + Поставке проналаска грешака + Онемогући ХТТП компресију + ХТТП компресија је онемогућена (режим проналаска грешака) + ХТТП компресија се користи кад год је могуће + Бележи мрежни саобраћај + Сав мрежни саобраћај се исцрпно бележи у дневник (режим проналаска грешака) + Мрежни саобраћај се не бележи у дневник + Пријави грешку + +
diff --git a/app/src/main/res/values-zh-rcn/strings.xml b/app/src/main/res/values-zh-rcn/strings.xml new file mode 100644 index 00000000..48694a4c --- /dev/null +++ b/app/src/main/res/values-zh-rcn/strings.xml @@ -0,0 +1,126 @@ + + + + + DAVdroid + + DAVdroid 网站 + 继续 + 帮助 + + HTTP 错误: %s + 服务器缺少功能: %s + I/O 错误: %s + URI 无效: %s + + + 管理同步账户 + + 感谢您在 Google Play 上购买 DAVdroid 支持本项目。然而 Google Play 版应用有两个问题:

+ +

1. 账户可能在重启后消失

+

您可能会遇到以下问题:您所有的 DAVdroid 同步账户(包括通讯录和日历)在设备重启后消失。这是由于 Android 系统的一个 BUG,开机时,在检查完孤立账户后才会载入(加密的)程序文件,从而导致了付费应用创建的账户被移除。

+

受影响的用户:
+ * 从 Play 商店安装 DAVdroid 的所有 Android 4.1 用户;
+ * 从 Play 商店安装 DAVdroid 的部分设备(如大部分的三星设备)的 Android 4.2 用户。

+ +

2. 账户可能会在升级 DAVdroid 后消失

+

您可能会遇到以下问题:您所有的 DAVdroid 同步账户(包括通讯录和日历)在使用 Play 商店升级 DAVdroid 后消失。这是 Android 系统的另一个 BUG,由于相似的原因,使得在升级应用后,付费应用创建的账户被移除。

+

受影响的用户:从 Play 商店安装 DAVdroid 的部分 Android 4.4.2 用户(如 Nexus 设备和 Moto G)。

+ +

如果您遇到了这些问题,请安装 + DAVdroid JB 修复程序

+ ]]>
+ 欢迎使用 DAVdroid/%s! + +

DAVdroid 是一个 Android 4+ 的 CalDAV/CardDAV 同步程序。开始使用本程序,只需增加一个 DAVdroid 账户,在其中设置 CalDAV/CardDAV 服务器,您的通讯录和日程就可以被双向同步了。

+ +

更多信息请查阅 DAVdroid 英文主页,其中包括首次使用的配置指南。DAVdroid 尊重您的隐私,请查阅我们的隐私政策

+ +

如果您使用 CyanogenMod 等修改版系统,请停用针对 DAVdroid 的“隐私防护”功能,否则 DAVdroid 无法访问、同步您的通讯录和日程。

+ +

如果您在使用中遇到问题,请先阅读 FAQ。如果您遇到了明显与 DAVdroid 有关的 BUG,请在 Github issues 上提交,不要直接联系我们,甚至给应用差评。

+ +

开源

+

DAVdroid 从一开始就是开源项目。您可以自己编译应用,并可以没有限制地免费使用。源代码 + 存放在 Github 上,您也可以 + 在 F-droid 上下载应用

+ +

然而,编写这个应用是一个大工程,所以我们已经决定把它作为付费应用放在应用商店上,收一笔小费。 + 如果您想支持这个项目,请考虑 给 DAVdroid 捐款 或购买其付费版。

+ +

许可

+

Copyright (c) 2013 – 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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Translation for German is provided by the authors. Translation for Chinese Simplified is by @phy25. Translations to other languages have been contributed by + various people which are mentioned in their respective language version.

+ +

使用的第三方程序库

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + 使用邮箱地址登录 + 服务器信息会通过域名进行发现。例如: myaccount@icloud.com + 使用 URL 和用户名登录 + 服务器信息会通过 URL 和用户名进行发现。个人搭建的服务通常使用此项。 + + 请输入您的邮箱地址。邮箱的域名会被用来自动发现服务器信息。 + Email: + + + http:// + https:// + + "如果不使用加密连接 (HTTPS),其他人将很容易获取到你的登录信息、通讯录和日程。" + 用户名: + 服务器根地址(集合会自动检测): + 抢先认证模式(推荐使用,但不兼容 Digest 认证方式) + + 密码: + + DAVdroid: 选择同步项 + 找不到可用的 CalDAV-/CardDAV 服务。 + 增加账户 + 正在请求,请稍等… + 需要同步哪些集合? + 通讯录 + 通讯录 + 日历 + 日历 + 最多选择一个通讯录:(再次点按可取消选择) + 选择日历: + + 账户信息 + 账户显示名: + 我的 CalDAV/CardDAV 账户 + Email 地址: + "日程的组织者 (ORGANIZER);如果您使用参与者信息,则必填" + "请使用您的 E-mail 地址作为账户名,因为 Android 会将帐户名用于您创建的日程的参与者 (ORGANIZER) 项。您不能有两个重名的账户。 + 只读 + + + 普通设置 + 调试设置 + 停用 HTTP 压缩 + HTTP 压缩已停用(调试模式) + HTTP 压缩会在可用时使用 + 记录网络传输 + 传输内容会被日志记录(调试模式) + 传输内容不会被日志记录 + 报告问题 + +
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..91a64e75 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,140 @@ + + + + + DAVdroid + + DAVdroid Web site + Next + Help + + HTTP error: %s + Missing capabilities: %s + I/O error: %s + Invalid URI: %s + + + Manage sync accounts + + Thank you for buying DAVdroid via Google Play and thus supporting the project. Unfortunately, there are two issues with Google Play:

+ +

1. Accounts may be gone after a reboot

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) are gone + after rebooting your device. The reason is a bug in Android + that causes accounts of paid apps to be removed on start-up because the (encrypted) APK files are + loaded after checking for orphaned accounts.

+

Affected users:
+ * all Android 4.1 users who have got DAVdroid from Play Store;
+ * Android 4.2 users who have got DAVdroid from Play Store only with certain devices (for instance, most Samsung devices)

+ +

2. Accounts may be gone after upgrading DAVdroid

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) when Play Store + updates DAVdroid. The reason is another bug in Android + that causes accounts of paid apps to be removed when upgrading for a similar reason.

+

Affected users: some Android 4.4.2 users who have got DAVdroid from Play Store (known for Nexus devices and Moto G)

+ +

If you\'re affected by one of these bugs, please install the + DAVdroid JB Workaround.

+ ]]>
+ Welcome to DAVdroid/%s! + +

DAVdroid is an Android 4+ sync adapter for CalDAV/CardDAV. To use it, just add a DAVdroid account + for your CalDAV/CardDAV server and your contacts/events will be synchronized in both directions.

+ +

For more information, please see the DAVdroid homepage. + There\'s a Setup guide, too. DAVdroid respects + your privacy, see our Privacy Policy.

+ +

If you use CyanogenMod, "Privacy Guard" must be disabled for DAVdroid. Otherwise, DAVdroid is not allowed to access + and synchronize your contacts and events.

+ +

In case of problems, please read the FAQ first. + If you encounter a bug that is clearly related to DAVdroid, enter it on + Github issues instead of contacting us directly or giving a poor + rating for the app.

+ +

Open-source

+

DAVdroid is designed to be an open-source project from the very first beginning. It is always possible to compile the + app yourself and use it for free without any obligations. The source code is + available on Github, and you can + download the app on F-droid.

+ +

However, it was much work to create this app, so we have decided to put it into the commercial stores for a small fee. + If you want to support this project, please consider donating to DAVdroid + or purchasing it.

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Translation for German is provided by the authors. Translations to other languages have been contributed by + various people which are mentioned in their respective language version.

+ +

Used third-party libraries

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Login with email address + Service details will be auto-detected by domain name. Example: myaccount@icloud.com + Login with URL and user name + Service details will be auto-detected by initial URL and user name. Mostly used for self-hosted services. + + Please enter your email address. Its domain name will be used to auto-detect service settings. + Email: + + + http:// + https:// + + "If you don't use encryption (HTTPS), other people may easily intercept your login details, contacts and events." + User name: + Base URL (collections will be auto-detected): + Preemptive authentication (recommended, but incompatible with Digest auth) + + Password: + + DAVdroid: Select collections + No CalDAV-/CardDAV service is available at this location. + Add account + Querying server. Please wait… + Which collections shall be synchronized? + Address books + Address book + Calendars + Calendar + Select up to one address book (tap again to unselect): + Select your calendars: + + Account details + Account name: + My CalDAV/CardDAV Account + Email address: + "ORGANIZER of your events; required if you use attendee info" + "Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can't have two accounts with the same name. + read-only + + + General settings + Debug settings + Disable HTTP compression + HTTP compression is disabled (debug mode) + HTTP compression is used whenever possible + Log network traffic + All network traffic is being logged verbosely (debug mode) + Network traffic is not being logged + Report an issue + +
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..a9661327 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/account_authenticator.xml b/app/src/main/res/xml/account_authenticator.xml new file mode 100644 index 00000000..542950e6 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator.xml @@ -0,0 +1,6 @@ + diff --git a/app/src/main/res/xml/account_prefs.xml b/app/src/main/res/xml/account_prefs.xml new file mode 100644 index 00000000..101de536 --- /dev/null +++ b/app/src/main/res/xml/account_prefs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/contacts.xml b/app/src/main/res/xml/contacts.xml new file mode 100644 index 00000000..10d6eb36 --- /dev/null +++ b/app/src/main/res/xml/contacts.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/general_settings.xml b/app/src/main/res/xml/general_settings.xml new file mode 100644 index 00000000..be95c323 --- /dev/null +++ b/app/src/main/res/xml/general_settings.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_calendars.xml b/app/src/main/res/xml/sync_calendars.xml new file mode 100644 index 00000000..d2ac9b79 --- /dev/null +++ b/app/src/main/res/xml/sync_calendars.xml @@ -0,0 +1,7 @@ + diff --git a/app/src/main/res/xml/sync_contacts.xml b/app/src/main/res/xml/sync_contacts.xml new file mode 100644 index 00000000..3a35ac92 --- /dev/null +++ b/app/src/main/res/xml/sync_contacts.xml @@ -0,0 +1,7 @@ + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..d6af282e --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + } +} + +allprojects { + repositories { + jcenter() + mavenCentral() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..8c0fb64a Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..0c71e760 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/import-summary.txt b/import-summary.txt new file mode 100644 index 00000000..78d25e5f --- /dev/null +++ b/import-summary.txt @@ -0,0 +1,741 @@ +ECLIPSE ANDROID PROJECT IMPORT SUMMARY +====================================== + +Ignored Files: +-------------- +The following files were *not* copied into the new Gradle project; you +should evaluate whether these are still needed in your project and if +so manually move them: + +* .gitignore +* .idea/ +* .idea/.name +* .idea/compiler.xml +* .idea/copyright/ +* .idea/copyright/profiles_settings.xml +* .idea/davdroid.iml +* .idea/encodings.xml +* .idea/misc.xml +* .idea/modules.xml +* .idea/scopes/ +* .idea/scopes/scope_settings.xml +* .idea/vcs.xml +* .idea/workspace.xml +* CONTRIBUTING.md +* COPYING +* README.md +* build.xml +* compile-libs/ +* compile-libs/lombok.jar +* doc/ +* doc/NIST.SP.800-52r1.pdf +* doc/how_davdroid_works.svgz +* doc/javadoc/ +* doc/javadoc/allclasses-frame.html +* doc/javadoc/allclasses-noframe.html +* doc/javadoc/at/ +* doc/javadoc/at/bitfire/ +* doc/javadoc/at/bitfire/davdroid/ +* doc/javadoc/at/bitfire/davdroid/ArrayUtils.html +* doc/javadoc/at/bitfire/davdroid/Constants.html +* doc/javadoc/at/bitfire/davdroid/MainActivity.html +* doc/javadoc/at/bitfire/davdroid/URIUtils.html +* doc/javadoc/at/bitfire/davdroid/class-use/ +* doc/javadoc/at/bitfire/davdroid/class-use/ArrayUtils.html +* doc/javadoc/at/bitfire/davdroid/class-use/Constants.html +* doc/javadoc/at/bitfire/davdroid/class-use/MainActivity.html +* doc/javadoc/at/bitfire/davdroid/class-use/URIUtils.html +* doc/javadoc/at/bitfire/davdroid/package-frame.html +* doc/javadoc/at/bitfire/davdroid/package-summary.html +* doc/javadoc/at/bitfire/davdroid/package-tree.html +* doc/javadoc/at/bitfire/davdroid/package-use.html +* doc/javadoc/at/bitfire/davdroid/resource/ +* doc/javadoc/at/bitfire/davdroid/resource/CalDavCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/CardDavAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/Contact.html +* doc/javadoc/at/bitfire/davdroid/resource/Event.html +* doc/javadoc/at/bitfire/davdroid/resource/InvalidResourceException.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalStorageException.html +* doc/javadoc/at/bitfire/davdroid/resource/RecordNotFoundException.html +* doc/javadoc/at/bitfire/davdroid/resource/RemoteCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/Resource.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/ +* doc/javadoc/at/bitfire/davdroid/resource/class-use/CalDavCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/CardDavAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Contact.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Event.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/InvalidResourceException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalStorageException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/RecordNotFoundException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/RemoteCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Resource.html +* doc/javadoc/at/bitfire/davdroid/resource/package-frame.html +* doc/javadoc/at/bitfire/davdroid/resource/package-summary.html +* doc/javadoc/at/bitfire/davdroid/resource/package-tree.html +* doc/javadoc/at/bitfire/davdroid/resource/package-use.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.AccountAuthenticator.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountSettings.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AddAccountActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.SyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.ContactsSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/DavSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/EnterCredentialsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.GeneralSettingsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.ServerInfoLoader.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.ResourceInfo.Type.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.ResourceInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SyncManager.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountAuthenticatorService.AccountAuthenticator.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountAuthenticatorService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountDetailsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountSettings.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AddAccountActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/CalendarsSyncAdapterService.SyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/CalendarsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ContactsSyncAdapterService.ContactsSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ContactsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/DavSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/EnterCredentialsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/GeneralSettingsActivity.GeneralSettingsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/GeneralSettingsActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/QueryServerDialogFragment.ServerInfoLoader.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/QueryServerDialogFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SelectCollectionsAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SelectCollectionsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.ResourceInfo.Type.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.ResourceInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SyncManager.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/WebDavResourceAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-frame.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-summary.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-tree.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-use.html +* doc/javadoc/at/bitfire/davdroid/webdav/ +* doc/javadoc/at/bitfire/davdroid/webdav/DavAddressbookMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavCalendarMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHref.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHttpClient.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavIncapableException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultiget.Type.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultistatus.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavNoContentException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavNoMultiStatusException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavAddressbookHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavCalendarHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavCurrentUserPrincipal.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropAddressData.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropAddressbookDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarColor.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarData.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarTimezone.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropComp.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropDisplayName.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropGetCTag.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropGetETag.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivAll.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivBind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivUnbind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivWrite.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivWriteContent.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.Addressbook.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.Calendar.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavPropstat.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavResponse.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpException.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpPropfind.Mode.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpReport.html +* doc/javadoc/at/bitfire/davdroid/webdav/NotFoundException.html +* doc/javadoc/at/bitfire/davdroid/webdav/PreconditionFailedException.html +* doc/javadoc/at/bitfire/davdroid/webdav/TlsSniSocketFactory.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.Property.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.PutMode.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/ +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavAddressbookMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavCalendarMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHref.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHttpClient.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHttpRequestRetryHandler.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavIncapableException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultiget.Type.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultistatus.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavNoContentException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavNoMultiStatusException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavAddressbookHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavCalendarHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavCurrentUserPrincipal.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropAddressData.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropAddressbookDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarColor.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarData.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarTimezone.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropComp.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropDisplayName.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropGetCTag.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropGetETag.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivAll.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivBind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivUnbind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivWrite.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivWriteContent.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.Addressbook.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.Calendar.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavPropstat.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavResponse.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpPropfind.Mode.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpReport.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/NotFoundException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/PreconditionFailedException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/TlsSniSocketFactory.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.Property.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.PutMode.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-frame.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-summary.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-tree.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-use.html +* doc/javadoc/constant-values.html +* doc/javadoc/deprecated-list.html +* doc/javadoc/help-doc.html +* doc/javadoc/index-files/ +* doc/javadoc/index-files/index-1.html +* doc/javadoc/index-files/index-10.html +* doc/javadoc/index-files/index-11.html +* doc/javadoc/index-files/index-12.html +* doc/javadoc/index-files/index-13.html +* doc/javadoc/index-files/index-14.html +* doc/javadoc/index-files/index-15.html +* doc/javadoc/index-files/index-16.html +* doc/javadoc/index-files/index-17.html +* doc/javadoc/index-files/index-18.html +* doc/javadoc/index-files/index-19.html +* doc/javadoc/index-files/index-2.html +* doc/javadoc/index-files/index-20.html +* doc/javadoc/index-files/index-21.html +* doc/javadoc/index-files/index-22.html +* doc/javadoc/index-files/index-23.html +* doc/javadoc/index-files/index-24.html +* doc/javadoc/index-files/index-3.html +* doc/javadoc/index-files/index-4.html +* doc/javadoc/index-files/index-5.html +* doc/javadoc/index-files/index-6.html +* doc/javadoc/index-files/index-7.html +* doc/javadoc/index-files/index-8.html +* doc/javadoc/index-files/index-9.html +* doc/javadoc/index.html +* doc/javadoc/overview-frame.html +* doc/javadoc/overview-summary.html +* doc/javadoc/overview-tree.html +* doc/javadoc/package-list +* doc/javadoc/resources/ +* doc/javadoc/resources/background.gif +* doc/javadoc/resources/tab.gif +* doc/javadoc/resources/titlebar.gif +* doc/javadoc/resources/titlebar_end.gif +* doc/javadoc/serialized-form.html +* doc/javadoc/stylesheet.css +* doc/rfc3744-webdav-access-control-protocol.txt +* doc/rfc4791-caldav.txt +* doc/rfc4918-webdav.txt +* doc/rfc5397-webdav-current-principal-extension.txt +* doc/rfc5785-well-known-uris.txt +* doc/rfc6352-carddav.txt +* doc/rfc6764-caldav-carddav-service-discovery.txt +* eclipse-libs/ +* eclipse-libs/lombok-api.jar +* private/ +* private/contacts2.db +* proguard-project.txt +* proguard/ +* proguard/dump.txt +* proguard/mapping.txt +* proguard/seeds.txt +* proguard/usage.txt +* target/ +* target/davdroid-0.1.apk +* target/davdroid-0.2-alpha.apk +* target/davdroid-0.3-alpha.apk +* target/davdroid_0_3_3_alpha.apk +* target/davdroid_0_3_4_alpha.apk +* target/davdroid_0_3_5_alpha.apk +* target/davdroid_0_3_6_alpha.apk +* target/davdroid_0_3_7_alpha.apk +* target/davdroid_0_3_8_alpha.apk +* target/davdroid_0_4_1_alpha.apk +* target/davdroid_0_4_2_alpha.apk +* target/davdroid_0_4_3_alpha.apk +* target/davdroid_0_4_4_alpha.apk +* target/davdroid_0_4_alpha.apk +* target/davdroid_0_5_10.apk +* target/davdroid_0_5_10_1.apk +* target/davdroid_0_5_10_2.apk +* target/davdroid_0_5_11.apk +* target/davdroid_0_5_12.apk +* target/davdroid_0_5_13.apk +* target/davdroid_0_5_14.apk +* target/davdroid_0_5_1_alpha.apk +* target/davdroid_0_5_2_alpha.apk +* target/davdroid_0_5_3_alpha.apk +* target/davdroid_0_5_4_alpha.apk +* target/davdroid_0_5_5_alpha.apk +* target/davdroid_0_5_6_alpha.apk +* target/davdroid_0_5_7_alpha.apk +* target/davdroid_0_5_8.apk +* target/davdroid_0_5_8_1.apk +* target/davdroid_0_5_9.apk +* target/davdroid_0_5_alpha.apk +* target/davdroid_0_6.apk +* target/davdroid_0_6_1.apk +* target/davdroid_0_6_2.apk +* target/davdroid_0_6_3.apk +* target/davdroid_0_6_4.apk +* target/davdroid_0_6_5.apk +* target/davdroid_0_6_6.apk +* target/davdroid_0_6_7.apk +* target/davdroid_0_6_7_1.apk +* target/davdroid_0_6_8.apk +* test/ +* test/.classpath +* test/.project +* test/.settings/ +* test/.settings/org.eclipse.jdt.core.prefs +* test/assets/ +* test/assets/all-day-0sec.ics +* test/assets/all-day-10days.ics +* test/assets/all-day-1day.ics +* test/assets/event-on-that-day.ics +* test/assets/impp.vcf +* test/assets/invalid-unknown-properties.vcf +* test/assets/reference.vcf +* test/assets/test.random +* test/assets/vcard3-sample1.vcf +* test/assets/vienna-evolution.ics +* test/bin/ +* test/bin/AndroidManifest.xml +* test/bin/classes.dex +* test/bin/classes/ +* test/bin/classes/at/ +* test/bin/classes/at/bitfire/ +* test/bin/classes/at/bitfire/davdroid/ +* test/bin/classes/at/bitfire/davdroid/resource/ +* test/bin/classes/at/bitfire/davdroid/resource/test/ +* test/bin/classes/at/bitfire/davdroid/resource/test/ContactTest.class +* test/bin/classes/at/bitfire/davdroid/resource/test/EventTest.class +* test/bin/classes/at/bitfire/davdroid/resource/test/LocalCalendarTest.class +* test/bin/classes/at/bitfire/davdroid/syncadapter/ +* test/bin/classes/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.class +* test/bin/classes/at/bitfire/davdroid/test/ +* test/bin/classes/at/bitfire/davdroid/test/ArrayUtilsTest.class +* test/bin/classes/at/bitfire/davdroid/test/BuildConfig.class +* test/bin/classes/at/bitfire/davdroid/test/Constants.class +* test/bin/classes/at/bitfire/davdroid/test/ContactTest.class +* test/bin/classes/at/bitfire/davdroid/test/R$attr.class +* test/bin/classes/at/bitfire/davdroid/test/R$drawable.class +* test/bin/classes/at/bitfire/davdroid/test/R$string.class +* test/bin/classes/at/bitfire/davdroid/test/R.class +* test/bin/classes/at/bitfire/davdroid/test/URLUtilsTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/ +* test/bin/classes/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/WebDavResourceTest.class +* test/bin/davdroidTest.apk +* test/bin/res/ +* test/bin/res/crunch/ +* test/bin/res/crunch/drawable-hdpi/ +* test/bin/res/crunch/drawable-hdpi/ic_launcher.png +* test/bin/res/crunch/drawable-ldpi/ +* test/bin/res/crunch/drawable-ldpi/ic_launcher.png +* test/bin/res/crunch/drawable-mdpi/ +* test/bin/res/crunch/drawable-mdpi/ic_launcher.png +* test/bin/res/crunch/drawable-xhdpi/ +* test/bin/res/crunch/drawable-xhdpi/ic_launcher.png +* test/bin/resources.ap_ +* test/gen/ +* test/gen/at/ +* test/gen/at/bitfire/ +* test/gen/at/bitfire/davdroid/ +* test/gen/at/bitfire/davdroid/test/ +* test/gen/at/bitfire/davdroid/test/BuildConfig.java +* test/gen/at/bitfire/davdroid/test/R.java +* test/proguard-project.txt +* test/project.properties +* test/robohydra/ +* test/robohydra/.gitignore +* test/robohydra/davdroid.conf +* test/robohydra/node_modules/ +* test/robohydra/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/ +* test/robohydra/node_modules/robohydra/.npmignore +* test/robohydra/node_modules/robohydra/ChangeLog +* test/robohydra/node_modules/robohydra/LICENSE +* test/robohydra/node_modules/robohydra/README.md +* test/robohydra/node_modules/robohydra/bin/ +* test/robohydra/node_modules/robohydra/bin/robohydra.js +* test/robohydra/node_modules/robohydra/bin/robohydra.js +* test/robohydra/node_modules/robohydra/examples/ +* test/robohydra/node_modules/robohydra/examples/README.md +* test/robohydra/node_modules/robohydra/examples/custom-types.conf +* test/robohydra/node_modules/robohydra/examples/delayed-proxy.conf +* test/robohydra/node_modules/robohydra/examples/empty-https.conf +* test/robohydra/node_modules/robohydra/examples/empty.conf +* test/robohydra/node_modules/robohydra/examples/external-scenarios.conf +* test/robohydra/node_modules/robohydra/examples/just-scenarios.conf +* test/robohydra/node_modules/robohydra/examples/plugins/ +* test/robohydra/node_modules/robohydra/examples/plugins/basic-scenario/ +* test/robohydra/node_modules/robohydra/examples/plugins/basic-scenario/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/custom-types/ +* test/robohydra/node_modules/robohydra/examples/plugins/custom-types/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/delayed-proxy/ +* test/robohydra/node_modules/robohydra/examples/plugins/delayed-proxy/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/etc/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/tada.png +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/scenarios/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/scenarios/externalScenarioExample.js +* test/robohydra/node_modules/robohydra/examples/plugins/request-finder/ +* test/robohydra/node_modules/robohydra/examples/plugins/request-finder/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-filtering/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-filtering/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-i18n/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-i18n/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-streaming/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-streaming/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-summoner/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-summoner/index.js +* test/robohydra/node_modules/robohydra/examples/request-finder.conf +* test/robohydra/node_modules/robohydra/examples/simple-filtering.conf +* test/robohydra/node_modules/robohydra/examples/simple-i18n.conf +* test/robohydra/node_modules/robohydra/examples/simple-i18n/ +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.en.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.es.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/just-french.fr.html +* test/robohydra/node_modules/robohydra/examples/simple-streaming.conf +* test/robohydra/node_modules/robohydra/examples/simple-summoner.conf +* test/robohydra/node_modules/robohydra/examples/test-cert.pem +* test/robohydra/node_modules/robohydra/examples/test-key.pem +* test/robohydra/node_modules/robohydra/lib/ +* test/robohydra/node_modules/robohydra/lib/exceptions.js +* test/robohydra/node_modules/robohydra/lib/heads.js +* test/robohydra/node_modules/robohydra/lib/plugins/ +* test/robohydra/node_modules/robohydra/lib/plugins/admin.js +* test/robohydra/node_modules/robohydra/lib/plugins/static/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/lipstick.css +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/robohydra.css +* test/robohydra/node_modules/robohydra/lib/plugins/static/favicon.ico +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/glyphicons-halflings-white.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/glyphicons-halflings.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/robohydra.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/js/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/js/robohydra.js +* test/robohydra/node_modules/robohydra/lib/plugins/templates/ +* test/robohydra/node_modules/robohydra/lib/plugins/templates/_header.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/_scenario-result.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/index.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/layout-main.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/scenario-instructions.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/scenarios.ejs +* test/robohydra/node_modules/robohydra/lib/robohydra.js +* test/robohydra/node_modules/robohydra/lib/robohydrasummoner.js +* test/robohydra/node_modules/robohydra/lib/utils.js +* test/robohydra/node_modules/robohydra/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/node_modules/commander/ +* test/robohydra/node_modules/robohydra/node_modules/commander/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/commander/index.js +* test/robohydra/node_modules/robohydra/node_modules/commander/package.json +* test/robohydra/node_modules/robohydra/node_modules/ejs/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/.gitmodules +* test/robohydra/node_modules/robohydra/node_modules/ejs/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/ejs/.travis.yml +* test/robohydra/node_modules/robohydra/node_modules/ejs/History.md +* test/robohydra/node_modules/robohydra/node_modules/ejs/Makefile +* test/robohydra/node_modules/robohydra/node_modules/ejs/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/ejs/benchmark.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/ejs.min.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/client.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/functions.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/functions.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/list.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/list.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/index.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/filters.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/utils.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/package.json +* test/robohydra/node_modules/robohydra/node_modules/ejs/support/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/support/compile.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/backslash.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/backslash.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/comments.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/comments.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/double-quote.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/double-quote.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/error.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/error.out +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/fail.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.css.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.css.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu-item.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu/item.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/menu.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/menu.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/messed.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/messed.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/newlines.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/newlines.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/no.newlines.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/no.newlines.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/para.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/pet.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/single-quote.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/single-quote.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/style.css +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/user.ejs +* test/robohydra/node_modules/robohydra/node_modules/markdown/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/markdown/.travis.yml +* test/robohydra/node_modules/robohydra/node_modules/markdown/Changes.markdown +* test/robohydra/node_modules/robohydra/node_modules/markdown/README.markdown +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/md2html.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/md2html.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/index.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/markdown.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/markdown-js.sublime-project +* test/robohydra/node_modules/robohydra/node_modules/markdown/markdown-js.sublime-workspace +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/README.md +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/examples/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/examples/my-program.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/lib/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/README.md +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/lib/abbrev.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/seed.yml +* test/robohydra/node_modules/robohydra/node_modules/mime/ +* test/robohydra/node_modules/robohydra/node_modules/mime/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/mime/README.md +* test/robohydra/node_modules/robohydra/node_modules/mime/mime.js +* test/robohydra/node_modules/robohydra/node_modules/mime/package.json +* test/robohydra/node_modules/robohydra/node_modules/mime/test.js +* test/robohydra/node_modules/robohydra/node_modules/mime/types/ +* test/robohydra/node_modules/robohydra/node_modules/mime/types/mime.types +* test/robohydra/node_modules/robohydra/node_modules/mime/types/node.types +* test/robohydra/node_modules/robohydra/node_modules/qs/ +* test/robohydra/node_modules/robohydra/node_modules/qs/.gitmodules +* test/robohydra/node_modules/robohydra/node_modules/qs/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/qs/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/qs/index.js +* test/robohydra/node_modules/robohydra/node_modules/qs/package.json +* test/robohydra/node_modules/robohydra/package.json +* test/robohydra/node_modules/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/plugins/README.md +* test/robohydra/node_modules/robohydra/plugins/frontend-dev-proxy/ +* test/robohydra/node_modules/robohydra/plugins/frontend-dev-proxy/index.js +* test/robohydra/node_modules/robohydra/plugins/logger/ +* test/robohydra/node_modules/robohydra/plugins/logger/index.js +* test/robohydra/node_modules/robohydra/plugins/no-caching/ +* test/robohydra/node_modules/robohydra/plugins/no-caching/index.js +* test/robohydra/node_modules/robohydra/plugins/replayer/ +* test/robohydra/node_modules/robohydra/plugins/replayer/index.js +* test/robohydra/node_modules/robohydra/test/ +* test/robohydra/node_modules/robohydra/test/adminhead-test.js +* test/robohydra/node_modules/robohydra/test/buster.js +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/basic.txt +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/etc/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/non-ascii.txt +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/index.js +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/tests/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/tests/fixtureLoader.js +* test/robohydra/node_modules/robohydra/test/head-test.js +* test/robohydra/node_modules/robohydra/test/helpers.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/customloadpath/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/customloadpath/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/right-robohydra-test/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/right-robohydra-test/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple-authenticator/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple-authenticator/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/url-query-authenticator/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/url-query-authenticator/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/wrong-fixed-picker/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/wrong-fixed-picker/index.js +* test/robohydra/node_modules/robohydra/test/plugins/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/scenarios/duplicateTestName.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/firstTest.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/secondTest.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/scenarios/external.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/scenarios/firstScenario.js +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/basic.txt +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/etc/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/non-ascii.txt +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/index.js +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/scenarios/fixtureLoader.js +* test/robohydra/node_modules/robohydra/test/robohydra-test.js +* test/robohydra/node_modules/robohydra/test/robohydrasummoner-test.js +* test/robohydra/plugins/ +* test/robohydra/plugins/assets/ +* test/robohydra/plugins/assets/index.js +* test/robohydra/plugins/dav-invalid/ +* test/robohydra/plugins/dav-invalid/index.js +* test/robohydra/plugins/dav/ +* test/robohydra/plugins/dav/index.js +* test/robohydra/plugins/headdav.js +* test/robohydra/plugins/redirect/ +* test/robohydra/plugins/redirect/index.js +* test/robohydra/plugins/simple.js +* test/robohydra/run.sh + +Moved Files: +------------ +Android Gradle projects use a different directory structure than ADT +Eclipse projects. Here's how the projects were restructured: + +* AndroidManifest.xml => app/src/main/AndroidManifest.xml +* assets/ => app/src/main/assets/ +* libs/backport-util-concurrent-3.1.jar => app/libs/backport-util-concurrent-3.1.jar +* libs/commons-codec-1.8.jar => app/libs/commons-codec-1.8.jar +* libs/commons-io-2.4.jar => app/libs/commons-io-2.4.jar +* libs/commons-lang-2.6.jar => app/libs/commons-lang-2.6.jar +* libs/commons-logging-1.1.3.jar => app/libs/commons-logging-1.1.3.jar +* libs/ez-vcard-0.9.6.jar => app/libs/ez-vcard-0.9.6.jar +* libs/httpclientandroidlib-1.2.1.jar => app/libs/httpclientandroidlib-1.2.1.jar +* libs/ical4j-1.0.6-davdroid141027.jar => app/libs/ical4j-1.0.6-davdroid141027.jar +* libs/org.xbill.dns_2.1.6.jar => app/libs/org.xbill.dns_2.1.6.jar +* libs/simple-xml-2.7.jar => app/libs/simple-xml-2.7.jar +* lint.xml => app/lint.xml +* res/ => app/src/main/res/ +* src/ => app/src/main/java/ +* test/res/ => app/src/androidTest/res/ +* test/src/ => app/src/androidTest/java/ + +Next Steps: +----------- +You can now build the project. The Gradle project needs network +connectivity to download dependencies. + +Bugs: +----- +If for some reason your project does not build, and you determine that +it is due to a bug or limitation of the Eclipse to Gradle importer, +please file a bug at http://b.android.com with category +Component-Tools. + +(This import summary is for your information only, and can be deleted +after import once you are satisfied with the results.) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'