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
86
.gitignore
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Created by https://www.gitignore.io
|
||||||
|
|
||||||
|
### Android ###
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
|
||||||
|
# Files for the Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
|
||||||
|
### Intellij ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
## Directory-based project format:
|
||||||
|
.idea/
|
||||||
|
# if you remove the above rule, at least ignore the following:
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
# .idea/workspace.xml
|
||||||
|
# .idea/tasks.xml
|
||||||
|
# .idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
# .idea/dataSources.ids
|
||||||
|
# .idea/dataSources.xml
|
||||||
|
# .idea/sqlDataSources.xml
|
||||||
|
# .idea/dynamic.xml
|
||||||
|
# .idea/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
# .idea/gradle.xml
|
||||||
|
# .idea/libraries
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
# .idea/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
|
||||||
|
|
||||||
|
### Gradle ###
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Ignore Gradle GUI config
|
||||||
|
gradle-app.setting
|
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
62
app/build.gradle
Normal file
@ -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'
|
||||||
|
}
|
||||||
|
}
|
5
app/lint.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="ExtraTranslation" severity="warning" />
|
||||||
|
<issue id="MissingTranslation" severity="warning" />
|
||||||
|
</lint>
|
@ -0,0 +1,65 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.resource.test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import ezvcard.property.Email;
|
||||||
|
import ezvcard.property.Telephone;
|
||||||
|
import lombok.Cleanup;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import at.bitfire.davdroid.resource.Contact;
|
||||||
|
import at.bitfire.davdroid.resource.InvalidResourceException;
|
||||||
|
|
||||||
|
public class ContactTest extends InstrumentationTestCase {
|
||||||
|
AssetManager assetMgr;
|
||||||
|
|
||||||
|
public void setUp() throws IOException, InvalidResourceException {
|
||||||
|
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReferenceVCard() throws IOException, InvalidResourceException {
|
||||||
|
Contact c = parseVCF("reference.vcf");
|
||||||
|
assertEquals("Gump", c.getFamilyName());
|
||||||
|
assertEquals("Forrest", c.getGivenName());
|
||||||
|
assertEquals("Forrest Gump", c.getDisplayName());
|
||||||
|
assertEquals("Bubba Gump Shrimp Co.", c.getOrganization().getValues().get(0));
|
||||||
|
assertEquals("Shrimp Man", c.getJobTitle());
|
||||||
|
|
||||||
|
Telephone phone1 = c.getPhoneNumbers().get(0);
|
||||||
|
assertEquals("(111) 555-1212", phone1.getText());
|
||||||
|
assertEquals("WORK", phone1.getParameters("TYPE").get(0));
|
||||||
|
assertEquals("VOICE", phone1.getParameters("TYPE").get(1));
|
||||||
|
|
||||||
|
Telephone phone2 = c.getPhoneNumbers().get(1);
|
||||||
|
assertEquals("(404) 555-1212", phone2.getText());
|
||||||
|
assertEquals("HOME", phone2.getParameters("TYPE").get(0));
|
||||||
|
assertEquals("VOICE", phone2.getParameters("TYPE").get(1));
|
||||||
|
|
||||||
|
Email email = c.getEmails().get(0);
|
||||||
|
assertEquals("forrestgump@example.com", email.getValue());
|
||||||
|
assertEquals("PREF", email.getParameters("TYPE").get(0));
|
||||||
|
assertEquals("INTERNET", email.getParameters("TYPE").get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseInvalidUnknownProperties() throws IOException, InvalidResourceException {
|
||||||
|
Contact c = parseVCF("invalid-unknown-properties.vcf");
|
||||||
|
assertEquals("VCard with invalid unknown properties", c.getDisplayName());
|
||||||
|
assertNull(c.getUnknownProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected Contact parseVCF(String fname) throws IOException, InvalidResourceException {
|
||||||
|
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
|
||||||
|
Contact c = new Contact(fname, null);
|
||||||
|
c.parseEntity(in);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.resource.test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import android.text.format.Time;
|
||||||
|
import at.bitfire.davdroid.resource.Event;
|
||||||
|
import at.bitfire.davdroid.resource.InvalidResourceException;
|
||||||
|
|
||||||
|
public class EventTest extends InstrumentationTestCase {
|
||||||
|
AssetManager assetMgr;
|
||||||
|
|
||||||
|
Event eViennaEvolution,
|
||||||
|
eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
|
||||||
|
|
||||||
|
public void setUp() throws IOException, InvalidResourceException {
|
||||||
|
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||||
|
|
||||||
|
eViennaEvolution = parseCalendar("vienna-evolution.ics");
|
||||||
|
eOnThatDay = parseCalendar("event-on-that-day.ics");
|
||||||
|
eAllDay1Day = parseCalendar("all-day-1day.ics");
|
||||||
|
eAllDay10Days = parseCalendar("all-day-10days.ics");
|
||||||
|
eAllDay0Sec = parseCalendar("all-day-0sec.ics");
|
||||||
|
|
||||||
|
//assertEquals("Test-Ereignis im schönen Wien", e.getSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testStartEndTimes() throws IOException, ParserException {
|
||||||
|
// event with start+end date-time
|
||||||
|
assertEquals(1381330800000L, eViennaEvolution.getDtStartInMillis());
|
||||||
|
assertEquals("Europe/Vienna", eViennaEvolution.getDtStartTzID());
|
||||||
|
assertEquals(1381334400000L, eViennaEvolution.getDtEndInMillis());
|
||||||
|
assertEquals("Europe/Vienna", eViennaEvolution.getDtEndTzID());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testStartEndTimesAllDay() throws IOException, ParserException {
|
||||||
|
// event with start date only
|
||||||
|
assertEquals(868838400000L, eOnThatDay.getDtStartInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtStartTzID());
|
||||||
|
// DTEND missing in VEVENT, must have been set to DTSTART+1 day
|
||||||
|
assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtEndTzID());
|
||||||
|
|
||||||
|
// event with start+end date for all-day event (one day)
|
||||||
|
assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtStartTzID());
|
||||||
|
assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtEndTzID());
|
||||||
|
|
||||||
|
// event with start+end date for all-day event (ten days)
|
||||||
|
assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtStartTzID());
|
||||||
|
assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtEndTzID());
|
||||||
|
|
||||||
|
// event with start+end date on some day (invalid 0 sec-event)
|
||||||
|
assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtStartTzID());
|
||||||
|
// DTEND invalid in VEVENT, must have been set to DTSTART+1 day
|
||||||
|
assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis());
|
||||||
|
assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtEndTzID());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTimezoneDefToTzId() {
|
||||||
|
// test valid definition
|
||||||
|
final String VTIMEZONE_SAMPLE = // taken from RFC 4791, 5.2.2. CALDAV:calendar-timezone Property
|
||||||
|
"BEGIN:VCALENDAR\n" +
|
||||||
|
"PRODID:-//Example Corp.//CalDAV Client//EN\n" +
|
||||||
|
"VERSION:2.0\n" +
|
||||||
|
"BEGIN:VTIMEZONE\n" +
|
||||||
|
"TZID:US-Eastern\n" +
|
||||||
|
"LAST-MODIFIED:19870101T000000Z\n" +
|
||||||
|
"BEGIN:STANDARD\n" +
|
||||||
|
"DTSTART:19671029T020000\n" +
|
||||||
|
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
|
||||||
|
"TZOFFSETFROM:-0400\n" +
|
||||||
|
"TZOFFSETTO:-0500\n" +
|
||||||
|
"TZNAME:Eastern Standard Time (US & 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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.resource.test;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.provider.CalendarContract;
|
||||||
|
import android.provider.CalendarContract.Attendees;
|
||||||
|
import android.provider.CalendarContract.Calendars;
|
||||||
|
import android.provider.CalendarContract.Events;
|
||||||
|
import android.provider.CalendarContract.Reminders;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import android.util.Log;
|
||||||
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
|
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||||
|
|
||||||
|
public class LocalCalendarTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
|
private static final String
|
||||||
|
TAG = "davroid.LocalCalendarTest",
|
||||||
|
calendarName = "DAVdroid_Test";
|
||||||
|
|
||||||
|
ContentProviderClient providerClient;
|
||||||
|
Account testAccount = new Account(calendarName, CalendarContract.ACCOUNT_TYPE_LOCAL);
|
||||||
|
LocalCalendar testCalendar;
|
||||||
|
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
private Uri syncAdapterURI(Uri uri) {
|
||||||
|
return uri.buildUpon()
|
||||||
|
.appendQueryParameter(Calendars.ACCOUNT_NAME, calendarName)
|
||||||
|
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
|
||||||
|
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
|
||||||
|
build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long insertNewEvent() throws LocalStorageException, RemoteException {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(Events.CALENDAR_ID, testCalendar.getId());
|
||||||
|
values.put(Events.TITLE, "Test Event");
|
||||||
|
values.put(Events.ALL_DAY, 0);
|
||||||
|
values.put(Events.DTSTART, Calendar.getInstance().getTimeInMillis());
|
||||||
|
values.put(Events.DTEND, Calendar.getInstance().getTimeInMillis());
|
||||||
|
values.put(Events.EVENT_TIMEZONE, "UTC");
|
||||||
|
values.put(Events.DIRTY, 1);
|
||||||
|
return ContentUris.parseId(providerClient.insert(syncAdapterURI(Events.CONTENT_URI), values));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteEvent(long id) throws RemoteException {
|
||||||
|
providerClient.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// initialization
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
|
||||||
|
protected void setUp() throws Exception {
|
||||||
|
ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
|
||||||
|
providerClient = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
||||||
|
|
||||||
|
long id;
|
||||||
|
|
||||||
|
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI,
|
||||||
|
new String[] { Calendars._ID },
|
||||||
|
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.NAME + "=?",
|
||||||
|
new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, calendarName },
|
||||||
|
null);
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
// found local test calendar
|
||||||
|
id = cursor.getLong(0);
|
||||||
|
Log.d(TAG, "Found test calendar with ID " + id);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// no local test calendar found, create
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put(Calendars.ACCOUNT_NAME, testAccount.name);
|
||||||
|
values.put(Calendars.ACCOUNT_TYPE, testAccount.type);
|
||||||
|
values.put(Calendars.NAME, calendarName);
|
||||||
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, calendarName);
|
||||||
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
|
||||||
|
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
|
||||||
|
values.put(Calendars.SYNC_EVENTS, 0);
|
||||||
|
values.put(Calendars.VISIBLE, 1);
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 15) {
|
||||||
|
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
|
||||||
|
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri calendarURI = providerClient.insert(syncAdapterURI(Calendars.CONTENT_URI), values);
|
||||||
|
|
||||||
|
id = ContentUris.parseId(calendarURI);
|
||||||
|
Log.d(TAG, "Created test calendar with ID " + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
testCalendar = new LocalCalendar(testAccount, providerClient, id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void tearDown() throws Exception {
|
||||||
|
Uri uri = ContentUris.withAppendedId(syncAdapterURI(Calendars.CONTENT_URI), testCalendar.getId());
|
||||||
|
providerClient.delete(uri, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// tests
|
||||||
|
|
||||||
|
public void testCTags() throws LocalStorageException {
|
||||||
|
assertNull(testCalendar.getCTag());
|
||||||
|
|
||||||
|
final String cTag = "just-modified";
|
||||||
|
testCalendar.setCTag(cTag);
|
||||||
|
|
||||||
|
assertEquals(cTag, testCalendar.getCTag());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testFindNew() throws LocalStorageException, RemoteException {
|
||||||
|
// at the beginning, there are no dirty events
|
||||||
|
assertTrue(testCalendar.findNew().length == 0);
|
||||||
|
assertTrue(testCalendar.findUpdated().length == 0);
|
||||||
|
|
||||||
|
// insert a "new" event
|
||||||
|
long id = insertNewEvent();
|
||||||
|
try {
|
||||||
|
// there must be one "new" event now
|
||||||
|
assertTrue(testCalendar.findNew().length == 1);
|
||||||
|
assertTrue(testCalendar.findUpdated().length == 0);
|
||||||
|
|
||||||
|
// nothing has changed, the record must still be "new"
|
||||||
|
// see issue #233
|
||||||
|
assertTrue(testCalendar.findNew().length == 1);
|
||||||
|
assertTrue(testCalendar.findUpdated().length == 0);
|
||||||
|
} finally {
|
||||||
|
deleteEvent(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package at.bitfire.davdroid.syncadapter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import at.bitfire.davdroid.resource.DavResourceFinder;
|
||||||
|
import at.bitfire.davdroid.resource.ServerInfo;
|
||||||
|
import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo;
|
||||||
|
import at.bitfire.davdroid.test.Constants;
|
||||||
|
import ezvcard.VCardVersion;
|
||||||
|
|
||||||
|
public class DavResourceFinderTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
|
DavResourceFinder finder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() {
|
||||||
|
finder = new DavResourceFinder(getInstrumentation().getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws IOException {
|
||||||
|
finder.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testFindResourcesRobohydra() throws Exception {
|
||||||
|
ServerInfo info = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true);
|
||||||
|
finder.findResources(info);
|
||||||
|
|
||||||
|
// CardDAV
|
||||||
|
assertTrue(info.isCardDAV());
|
||||||
|
List<ResourceInfo> collections = info.getAddressBooks();
|
||||||
|
assertEquals(1, collections.size());
|
||||||
|
|
||||||
|
assertEquals("Default Address Book", collections.get(0).getDescription());
|
||||||
|
assertEquals(VCardVersion.V4_0, collections.get(0).getVCardVersion());
|
||||||
|
|
||||||
|
// CalDAV
|
||||||
|
assertTrue(info.isCalDAV());
|
||||||
|
collections = info.getCalendars();
|
||||||
|
assertEquals(2, collections.size());
|
||||||
|
|
||||||
|
ResourceInfo resource = collections.get(0);
|
||||||
|
assertEquals("Private Calendar", resource.getTitle());
|
||||||
|
assertEquals("This is my private calendar.", resource.getDescription());
|
||||||
|
assertFalse(resource.isReadOnly());
|
||||||
|
|
||||||
|
resource = collections.get(1);
|
||||||
|
assertEquals("Work Calendar", resource.getTitle());
|
||||||
|
assertTrue(resource.isReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testGetInitialContextURL() throws Exception {
|
||||||
|
// without SRV records, but with well-known paths
|
||||||
|
ServerInfo roboHydra = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true);
|
||||||
|
assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "caldav"));
|
||||||
|
assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "carddav"));
|
||||||
|
|
||||||
|
// with SRV records and well-known paths
|
||||||
|
ServerInfo iCloud = new ServerInfo(new URI("mailto:test@icloud.com"), "", "", true);
|
||||||
|
assertEquals(new URI("https://contacts.icloud.com/"), finder.getInitialContextURL(iCloud, "carddav"));
|
||||||
|
assertEquals(new URI("https://caldav.icloud.com/"), finder.getInitialContextURL(iCloud, "caldav"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.test;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import at.bitfire.davdroid.ArrayUtils;
|
||||||
|
|
||||||
|
|
||||||
|
public class ArrayUtilsTest extends TestCase {
|
||||||
|
|
||||||
|
public void testPartition() {
|
||||||
|
// n == 0
|
||||||
|
assertTrue(Arrays.deepEquals(
|
||||||
|
new Long[0][0],
|
||||||
|
ArrayUtils.partition(new Long[] { }, 5)));
|
||||||
|
|
||||||
|
// n < max
|
||||||
|
assertTrue(Arrays.deepEquals(
|
||||||
|
new Long[][] { { 1l, 2l } },
|
||||||
|
ArrayUtils.partition(new Long[] { 1l, 2l }, 5)));
|
||||||
|
|
||||||
|
// n == max
|
||||||
|
assertTrue(Arrays.deepEquals(
|
||||||
|
new Long[][] { { 1l, 2l }, { 3l, 4l } },
|
||||||
|
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2)));
|
||||||
|
|
||||||
|
// n > max
|
||||||
|
assertTrue(Arrays.deepEquals(
|
||||||
|
new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } },
|
||||||
|
ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package at.bitfire.davdroid.test;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
public static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/";
|
||||||
|
|
||||||
|
public static URI roboHydra;
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
roboHydra = new URI(ROBOHYDRA_BASE);
|
||||||
|
} catch(URISyntaxException e) {
|
||||||
|
Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import at.bitfire.davdroid.resource.Contact;
|
||||||
|
import ezvcard.property.Impp;
|
||||||
|
|
||||||
|
public class ContactTest extends InstrumentationTestCase {
|
||||||
|
AssetManager assetMgr;
|
||||||
|
|
||||||
|
public void setUp() {
|
||||||
|
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIMPP() throws IOException {
|
||||||
|
Contact c = parseVCard("impp.vcf");
|
||||||
|
assertEquals("test mctest", c.getDisplayName());
|
||||||
|
|
||||||
|
Impp jabber = c.getImpps().get(0);
|
||||||
|
assertNull(jabber.getProtocol());
|
||||||
|
assertEquals("test-without-valid-scheme@test.tld", jabber.getHandle());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void testParseVcard3() throws IOException, ParserException {
|
||||||
|
Contact c = parseVCard("vcard3-sample1.vcf");
|
||||||
|
|
||||||
|
assertEquals("Forrest Gump", c.getDisplayName());
|
||||||
|
assertEquals("Forrest", c.getGivenName());
|
||||||
|
assertEquals("Gump", c.getFamilyName());
|
||||||
|
|
||||||
|
assertEquals(2, c.getPhoneNumbers().size());
|
||||||
|
assertEquals("(111) 555-1212", c.getPhoneNumbers().get(0).getText());
|
||||||
|
|
||||||
|
assertEquals(1, c.getEmails().size());
|
||||||
|
assertEquals("forrestgump@example.com", c.getEmails().get(0).getValue());
|
||||||
|
|
||||||
|
assertFalse(c.isStarred());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Contact parseVCard(String fileName) throws IOException {
|
||||||
|
@Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
Contact c = new Contact(fileName, null);
|
||||||
|
c.parseEntity(in);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.test;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import at.bitfire.davdroid.URIUtils;
|
||||||
|
|
||||||
|
|
||||||
|
public class URLUtilsTest extends TestCase {
|
||||||
|
|
||||||
|
/* RFC 1738 p17 HTTP URLs:
|
||||||
|
hpath = hsegment *[ "/" hsegment ]
|
||||||
|
hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ]
|
||||||
|
uchar = unreserved | escape
|
||||||
|
unreserved = alpha | digit | safe | extra
|
||||||
|
alpha = lowalpha | hialpha
|
||||||
|
lowalpha = ...
|
||||||
|
hialpha = ...
|
||||||
|
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
|
||||||
|
"8" | "9"
|
||||||
|
safe = "$" | "-" | "_" | "." | "+"
|
||||||
|
extra = "!" | "*" | "'" | "(" | ")" | ","
|
||||||
|
escape = "%" hex hex
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
public void testEnsureTrailingSlash() throws Exception {
|
||||||
|
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test"));
|
||||||
|
assertEquals("/test/", URIUtils.ensureTrailingSlash("/test/"));
|
||||||
|
|
||||||
|
String withoutSlash = "http://www.test.at/dav/collection",
|
||||||
|
withSlash = withoutSlash + "/";
|
||||||
|
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withoutSlash)));
|
||||||
|
assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withSlash)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testParseURI() throws Exception {
|
||||||
|
// don't escape valid characters
|
||||||
|
String validPath = "/;:@&=$-_.+!*'(),";
|
||||||
|
assertEquals(new URI("https://www.test.at:123" + validPath), URIUtils.parseURI("https://www.test.at:123" + validPath));
|
||||||
|
assertEquals(new URI(validPath), URIUtils.parseURI(validPath));
|
||||||
|
|
||||||
|
// keep literal IPv6 addresses (only in host name)
|
||||||
|
assertEquals(new URI("https://[1:2::1]/"), URIUtils.parseURI("https://[1:2::1]/"));
|
||||||
|
|
||||||
|
// ~ as home directory
|
||||||
|
assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/~user1/"));
|
||||||
|
assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/%7euser1/"));
|
||||||
|
|
||||||
|
// @ in directory name
|
||||||
|
assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user@server.com/"));
|
||||||
|
assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user%40server.com/"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import at.bitfire.davdroid.test.Constants;
|
||||||
|
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||||
|
import ch.boye.httpclientandroidlib.client.methods.HttpOptions;
|
||||||
|
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
|
||||||
|
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
|
||||||
|
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||||
|
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
|
||||||
|
import ch.boye.httpclientandroidlib.protocol.HttpContext;
|
||||||
|
|
||||||
|
public class DavRedirectStrategyTest extends TestCase {
|
||||||
|
|
||||||
|
CloseableHttpClient httpClient;
|
||||||
|
DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() {
|
||||||
|
httpClient = HttpClientBuilder.create()
|
||||||
|
.useSystemProperties()
|
||||||
|
.disableRedirectHandling()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws IOException {
|
||||||
|
httpClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// happy cases
|
||||||
|
|
||||||
|
public void testNonRedirection() throws Exception {
|
||||||
|
HttpUriRequest request = new HttpOptions(Constants.roboHydra);
|
||||||
|
HttpResponse response = httpClient.execute(request);
|
||||||
|
assertFalse(strategy.isRedirected(request, response, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDefaultRedirection() throws Exception {
|
||||||
|
final String newLocation = "/new-location";
|
||||||
|
|
||||||
|
HttpContext context = HttpClientContext.create();
|
||||||
|
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/301?to=" + newLocation));
|
||||||
|
HttpResponse response = httpClient.execute(request, context);
|
||||||
|
assertTrue(strategy.isRedirected(request, response, context));
|
||||||
|
|
||||||
|
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
|
||||||
|
assertEquals(Constants.roboHydra.resolve(newLocation), redirected.getURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// error cases
|
||||||
|
|
||||||
|
public void testMissingLocation() throws Exception {
|
||||||
|
HttpContext context = HttpClientContext.create();
|
||||||
|
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/without-location"));
|
||||||
|
HttpResponse response = httpClient.execute(request, context);
|
||||||
|
assertFalse(strategy.isRedirected(request, response, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testRelativeLocation() throws Exception {
|
||||||
|
HttpContext context = HttpClientContext.create();
|
||||||
|
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/relative"));
|
||||||
|
HttpResponse response = httpClient.execute(request, context);
|
||||||
|
assertTrue(strategy.isRedirected(request, response, context));
|
||||||
|
|
||||||
|
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
|
||||||
|
assertEquals(Constants.roboHydra.resolve("/new/location"), redirected.getURI());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
public class TlsSniSocketFactoryTest extends TestCase {
|
||||||
|
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
|
||||||
|
|
||||||
|
public void testCiphers() throws IOException {
|
||||||
|
SSLSocket socket = (SSLSocket)TlsSniSocketFactory.INSTANCE.createSocket(null);
|
||||||
|
|
||||||
|
Log.i(TAG, "Enabled:");
|
||||||
|
for (String cipher : socket.getEnabledCipherSuites())
|
||||||
|
Log.i(TAG, cipher);
|
||||||
|
|
||||||
|
Log.i(TAG, "Supported:");
|
||||||
|
for (String cipher : socket.getSupportedCipherSuites())
|
||||||
|
Log.i(TAG, cipher);
|
||||||
|
|
||||||
|
assert(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,233 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid.webdav;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.test.InstrumentationTestCase;
|
||||||
|
import at.bitfire.davdroid.test.Constants;
|
||||||
|
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
|
||||||
|
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||||
|
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
|
||||||
|
|
||||||
|
// tests require running robohydra!
|
||||||
|
|
||||||
|
public class WebDavResourceTest extends InstrumentationTestCase {
|
||||||
|
static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
|
||||||
|
|
||||||
|
final static String PATH_SIMPLE_FILE = "collection/new.file";
|
||||||
|
|
||||||
|
AssetManager assetMgr;
|
||||||
|
CloseableHttpClient httpClient;
|
||||||
|
|
||||||
|
WebDavResource baseDAV;
|
||||||
|
WebDavResource simpleFile,
|
||||||
|
davCollection, davNonExistingFile, davExistingFile,
|
||||||
|
davInvalid;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() throws Exception {
|
||||||
|
httpClient = DavHttpClient.create(true, true);
|
||||||
|
|
||||||
|
assetMgr = getInstrumentation().getContext().getResources().getAssets();
|
||||||
|
|
||||||
|
baseDAV = new WebDavResource(httpClient, Constants.roboHydra.resolve("/dav/"));
|
||||||
|
|
||||||
|
simpleFile = new WebDavResource(httpClient, Constants.roboHydra.resolve("assets/test.random"));
|
||||||
|
|
||||||
|
davCollection = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav/"));
|
||||||
|
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
|
||||||
|
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
|
||||||
|
|
||||||
|
davInvalid = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav-invalid/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws Exception {
|
||||||
|
httpClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* test feature detection */
|
||||||
|
|
||||||
|
public void testOptions() throws Exception {
|
||||||
|
String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE", "REPORT" },
|
||||||
|
davCapabilities = new String[] { "addressbook", "calendar-access" };
|
||||||
|
|
||||||
|
WebDavResource capable = new WebDavResource(baseDAV);
|
||||||
|
capable.options();
|
||||||
|
for (String davMethod : davMethods)
|
||||||
|
assertTrue(capable.supportsMethod(davMethod));
|
||||||
|
for (String capability : davCapabilities)
|
||||||
|
assertTrue(capable.supportsDAV(capability));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPropfindCurrentUserPrincipal() throws Exception {
|
||||||
|
davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
|
||||||
|
assertEquals("/dav/principals/users/test", davCollection.getCurrentUserPrincipal());
|
||||||
|
|
||||||
|
try {
|
||||||
|
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
|
||||||
|
fail();
|
||||||
|
|
||||||
|
} catch(DavException ex) {
|
||||||
|
}
|
||||||
|
assertNull(simpleFile.getCurrentUserPrincipal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPropfindHomeSets() throws Exception {
|
||||||
|
WebDavResource dav = new WebDavResource(davCollection, "principals/users/test");
|
||||||
|
dav.propfind(HttpPropfind.Mode.HOME_SETS);
|
||||||
|
assertEquals("/dav/addressbooks/test/", dav.getAddressbookHomeSet());
|
||||||
|
assertEquals("/dav/calendars/test/", dav.getCalendarHomeSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPropfindAddressBooks() throws Exception {
|
||||||
|
WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test");
|
||||||
|
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
|
||||||
|
assertEquals(2, dav.getMembers().size());
|
||||||
|
for (WebDavResource member : dav.getMembers()) {
|
||||||
|
if (member.getName().equals("default-v4.vcf"))
|
||||||
|
assertTrue(member.isAddressBook());
|
||||||
|
else
|
||||||
|
assertFalse(member.isAddressBook());
|
||||||
|
assertFalse(member.isCalendar());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPropfindCalendars() throws Exception {
|
||||||
|
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
|
||||||
|
dav.propfind(Mode.CALDAV_COLLECTIONS);
|
||||||
|
assertEquals(3, dav.getMembers().size());
|
||||||
|
assertEquals("0xFF00FF", dav.getMembers().get(2).getColor());
|
||||||
|
for (WebDavResource member : dav.getMembers()) {
|
||||||
|
if (member.getName().contains(".ics"))
|
||||||
|
assertTrue(member.isCalendar());
|
||||||
|
else
|
||||||
|
assertFalse(member.isCalendar());
|
||||||
|
assertFalse(member.isAddressBook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPropfindTrailingSlashes() throws Exception {
|
||||||
|
final String principalOK = "/principals/ok";
|
||||||
|
|
||||||
|
String requestPaths[] = {
|
||||||
|
"/dav/collection-response-with-trailing-slash",
|
||||||
|
"/dav/collection-response-with-trailing-slash/",
|
||||||
|
"/dav/collection-response-without-trailing-slash",
|
||||||
|
"/dav/collection-response-without-trailing-slash/"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String path : requestPaths) {
|
||||||
|
WebDavResource davSlash = new WebDavResource(davCollection, path);
|
||||||
|
davSlash.propfind(Mode.CARDDAV_COLLECTIONS);
|
||||||
|
assertEquals(principalOK, davSlash.getCurrentUserPrincipal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* test normal HTTP/WebDAV */
|
||||||
|
|
||||||
|
public void testPropfindRedirection() throws Exception {
|
||||||
|
// PROPFIND redirection
|
||||||
|
WebDavResource redirected = new WebDavResource(baseDAV, "/redirect/301?to=/dav/");
|
||||||
|
redirected.propfind(Mode.CURRENT_USER_PRINCIPAL);
|
||||||
|
assertEquals("/dav/", redirected.getLocation().getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGet() throws Exception {
|
||||||
|
simpleFile.get("*/*");
|
||||||
|
@Cleanup InputStream is = assetMgr.open("test.random", AssetManager.ACCESS_STREAMING);
|
||||||
|
byte[] expected = IOUtils.toByteArray(is);
|
||||||
|
assertTrue(Arrays.equals(expected, simpleFile.getContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testGetHttpsWithSni() throws Exception {
|
||||||
|
WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch"));
|
||||||
|
|
||||||
|
boolean sniWorking = false;
|
||||||
|
try {
|
||||||
|
file.get("*/*");
|
||||||
|
sniWorking = true;
|
||||||
|
} catch (SSLPeerUnverifiedException e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(sniWorking);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMultiGet() throws Exception {
|
||||||
|
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf");
|
||||||
|
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" });
|
||||||
|
assertEquals("My Book", davAddressBook.getDisplayName());
|
||||||
|
assertEquals(2, davAddressBook.getMembers().size());
|
||||||
|
for (WebDavResource member : davAddressBook.getMembers())
|
||||||
|
assertNotNull(member.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPutAddDontOverwrite() throws Exception {
|
||||||
|
// should succeed on a non-existing file
|
||||||
|
assertEquals("has-just-been-created", davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE));
|
||||||
|
|
||||||
|
// should fail on an existing file
|
||||||
|
try {
|
||||||
|
davExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
|
||||||
|
fail();
|
||||||
|
} catch(PreconditionFailedException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testPutUpdateDontOverwrite() throws Exception {
|
||||||
|
// should succeed on an existing file
|
||||||
|
assertEquals("has-just-been-updated", davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE));
|
||||||
|
|
||||||
|
// should fail on a non-existing file
|
||||||
|
try {
|
||||||
|
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
|
||||||
|
fail();
|
||||||
|
} catch(PreconditionFailedException ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testDelete() throws Exception {
|
||||||
|
// should succeed on an existing file
|
||||||
|
davExistingFile.delete();
|
||||||
|
|
||||||
|
// should fail on a non-existing file
|
||||||
|
try {
|
||||||
|
davNonExistingFile.delete();
|
||||||
|
fail();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* test CalDAV/CardDAV */
|
||||||
|
|
||||||
|
|
||||||
|
/* special test */
|
||||||
|
|
||||||
|
public void testInvalidURLs() throws Exception {
|
||||||
|
WebDavResource dav = new WebDavResource(davInvalid, "addressbooks/%7euser1/");
|
||||||
|
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
|
||||||
|
List<WebDavResource> members = dav.getMembers();
|
||||||
|
assertEquals(1, members.size());
|
||||||
|
assertEquals(Constants.ROBOHYDRA_BASE + "dav-invalid/addressbooks/~user1/My%20Contacts:1.vcf/", members.get(0).getLocation().toASCIIString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
app/src/androidTest/res/drawable-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
app/src/androidTest/res/drawable-ldpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
app/src/androidTest/res/drawable-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
app/src/androidTest/res/drawable-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
6
app/src/androidTest/res/values/strings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">DavdroidTest</string>
|
||||||
|
|
||||||
|
</resources>
|
79
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="at.bitfire.davdroid"
|
||||||
|
android:versionCode="47"
|
||||||
|
android:versionName="0.6.8" android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="14"
|
||||||
|
android:targetSdkVersion="21" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@drawable/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:process=":sync" >
|
||||||
|
<service
|
||||||
|
android:name=".syncadapter.AccountAuthenticatorService"
|
||||||
|
android:exported="false" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
|
android:resource="@xml/account_authenticator" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".syncadapter.ContactsSyncAdapterService"
|
||||||
|
android:exported="true" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/sync_contacts" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||||
|
android:resource="@xml/contacts" />
|
||||||
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||||
|
android:exported="true" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.content.SyncAdapter"
|
||||||
|
android:resource="@xml/sync_calendars" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".syncadapter.AddAccountActivity"
|
||||||
|
android:excludeFromRecents="true" >
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".syncadapter.GeneralSettingsActivity" >
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
33
app/src/main/java/at/bitfire/davdroid/ArrayUtils.java
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2014 Ricki Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
|
package at.bitfire.davdroid;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
|
||||||
|
public class ArrayUtils {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T[][] partition(T[] bigArray, int max) {
|
||||||
|
int nItems = bigArray.length;
|
||||||
|
int nPartArrays = (nItems + max-1)/max;
|
||||||
|
|
||||||
|
T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0);
|
||||||
|
|
||||||
|
// nItems is now the number of remaining items
|
||||||
|
for (int i = 0; nItems > 0; i++) {
|
||||||
|
int n = (nItems < max) ? nItems : max;
|
||||||
|
partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n);
|
||||||
|
System.arraycopy(bigArray, i*max, partArrays[i], 0, n);
|
||||||
|
|
||||||
|
nItems -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return partArrays;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
app/src/main/java/at/bitfire/davdroid/Constants.java
Normal file
@ -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";
|
||||||
|
}
|
82
app/src/main/java/at/bitfire/davdroid/MainActivity.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
53
app/src/main/java/at/bitfire/davdroid/URIUtils.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<Event> {
|
||||||
|
//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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<Contact> {
|
||||||
|
//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);
|
||||||
|
}
|
||||||
|
}
|
410
app/src/main/java/at/bitfire/davdroid/resource/Contact.java
Normal file
@ -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<Telephone> phoneNumbers = new LinkedList<Telephone>();
|
||||||
|
@Getter private List<Email> emails = new LinkedList<Email>();
|
||||||
|
@Getter private List<Impp> impps = new LinkedList<Impp>();
|
||||||
|
@Getter private List<Address> addresses = new LinkedList<Address>();
|
||||||
|
@Getter private List<String> categories = new LinkedList<String>();
|
||||||
|
@Getter private List<String> URLs = new LinkedList<String>();
|
||||||
|
|
||||||
|
|
||||||
|
/* 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<String> notes = new LinkedList<String>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<ServerInfo.ResourceInfo> addressBooks = new LinkedList<ServerInfo.ResourceInfo>();
|
||||||
|
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<ServerInfo.ResourceInfo> calendars = new LinkedList<ServerInfo.ResourceInfo>();
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
368
app/src/main/java/at/bitfire/davdroid/resource/Event.java
Normal file
@ -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<Attendee> attendees = new LinkedList<Attendee>();
|
||||||
|
public void addAttendee(Attendee attendee) {
|
||||||
|
attendees.add(attendee);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter private List<VAlarm> alarms = new LinkedList<VAlarm>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
1007
app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java
Normal file
@ -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<Event> {
|
||||||
|
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<LocalCalendar> calendars = new LinkedList<LocalCalendar>();
|
||||||
|
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<String> sqlFileNames = new LinkedList<String>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 <T> Subtype of Resource that can be stored in the collection
|
||||||
|
*/
|
||||||
|
public abstract class LocalCollection<T extends Resource> {
|
||||||
|
private static final String TAG = "davdroid.LocalCollection";
|
||||||
|
|
||||||
|
protected Account account;
|
||||||
|
protected ContentProviderClient providerClient;
|
||||||
|
protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<ContentProviderOperation>();
|
||||||
|
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 <T> Subtype of Resource that can be stored in the collection
|
||||||
|
*/
|
||||||
|
public abstract class RemoteCollection<T extends Resource> {
|
||||||
|
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<T> resources = new LinkedList<T>();
|
||||||
|
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<String> names = new LinkedList<String>();
|
||||||
|
for (Resource resource : resources)
|
||||||
|
names.add(resource.getName());
|
||||||
|
|
||||||
|
LinkedList<T> foundResources = new LinkedList<T>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
46
app/src/main/java/at/bitfire/davdroid/resource/Resource.java
Normal file
@ -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;
|
||||||
|
}
|
@ -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<ResourceInfo>
|
||||||
|
addressBooks = new LinkedList<ResourceInfo>(),
|
||||||
|
calendars = new LinkedList<ResourceInfo>();
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<LocalCollection<?>, 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<LocalCollection<?>, RemoteCollection<?>> map = new HashMap<LocalCollection<?>, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<LocalCollection<?>, 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<LocalCollection<?>, RemoteCollection<?>> map = new HashMap<LocalCollection<?>, RemoteCollection<?>>();
|
||||||
|
map.put(database, dav);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
Log.e(TAG, "Couldn't build address book URI", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<Void, Void, Void>() {
|
||||||
|
@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<LocalCollection<?>, 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<LocalCollection<?>, RemoteCollection<?>> syncCollections = getSyncPairs(account, provider);
|
||||||
|
if (syncCollections == null)
|
||||||
|
Log.i(TAG, "Nothing to synchronize");
|
||||||
|
else
|
||||||
|
try {
|
||||||
|
for (Map.Entry<LocalCollection<?>, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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<ServerInfo> {
|
||||||
|
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<ServerInfo> 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<ServerInfo> onCreateLoader(int id, Bundle args) {
|
||||||
|
Log.i(TAG, "onCreateLoader");
|
||||||
|
return new ServerInfoLoader(getActivity(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<ServerInfo> 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<ServerInfo> arg0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class ServerInfoLoader extends AsyncTaskLoader<ServerInfo> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "<b>" + title + "</b>";
|
||||||
|
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 + "<br/>" + 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<? extends Resource> local;
|
||||||
|
protected RemoteCollection<? extends Resource> remote;
|
||||||
|
|
||||||
|
|
||||||
|
public SyncManager(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> 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<Resource> remotelyAdded = new HashSet<Resource>(),
|
||||||
|
remotelyUpdated = new HashSet<Resource>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<WebDavResource> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
}
|
@ -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 {
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<ConnectionSocketFactory> socketFactoryRegistry;
|
||||||
|
|
||||||
|
static {
|
||||||
|
socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<DavHref> 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<DavHref>(names.length);
|
||||||
|
for (String name : names)
|
||||||
|
multiget.hrefs.add(new DavHref(name));
|
||||||
|
|
||||||
|
return multiget;
|
||||||
|
}
|
||||||
|
}
|
@ -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<DavResponse> response;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
211
app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java
Normal file
@ -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<Privilege> 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<AddressDataType> 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<Comp> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<DavPropstat> propstat;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
102
app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java
Normal file
@ -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<DavProp.Privilege>();
|
||||||
|
propfind.prop.addressbookDescription = new DavProp.AddressbookDescription();
|
||||||
|
propfind.prop.supportedAddressData = new LinkedList<DavProp.AddressDataType>();
|
||||||
|
break;
|
||||||
|
case CALDAV_COLLECTIONS:
|
||||||
|
depth = 1;
|
||||||
|
propfind.prop.displayname = new DavProp.DisplayName();
|
||||||
|
propfind.prop.resourcetype = new DavProp.ResourceType();
|
||||||
|
propfind.prop.currentUserPrivilegeSet = new LinkedList<DavProp.Privilege>();
|
||||||
|
propfind.prop.calendarDescription = new DavProp.CalendarDescription();
|
||||||
|
propfind.prop.calendarColor = new DavProp.CalendarColor();
|
||||||
|
propfind.prop.calendarTimezone = new DavProp.CalendarTimezone();
|
||||||
|
propfind.prop.supportedCalendarComponentSet = new LinkedList<DavProp.Comp>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
46
app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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<String> protocols = new LinkedList<String>();
|
||||||
|
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<String> 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<String> availableCiphers = Arrays.asList(ssl.getSupportedCipherSuites());
|
||||||
|
|
||||||
|
// preferred ciphers = allowed Ciphers \ availableCiphers
|
||||||
|
HashSet<String> preferredCiphers = new HashSet<String>(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<String> enabledCiphers = preferredCiphers;
|
||||||
|
enabledCiphers.addAll(new HashSet<String>(Arrays.asList(ssl.getEnabledCipherSuites())));
|
||||||
|
|
||||||
|
Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", "));
|
||||||
|
ssl.setEnabledCipherSuites(enabledCiphers.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
581
app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java
Normal file
@ -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<String> capabilities = new HashSet<String>(),
|
||||||
|
methods = new HashSet<String>();
|
||||||
|
|
||||||
|
// DAV properties
|
||||||
|
protected HashMap<Property, String> properties = new HashMap<Property, String>();
|
||||||
|
@Getter protected List<String> supportedComponents;
|
||||||
|
|
||||||
|
// list of members (only for collections)
|
||||||
|
@Getter protected List<WebDavResource> 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<String> hrefs = new LinkedList<String>();
|
||||||
|
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<WebDavResource> members = new LinkedList<WebDavResource>();
|
||||||
|
|
||||||
|
// 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<Property, String> properties = new HashMap<Property, String>();
|
||||||
|
List<String> 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<String>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
app/src/main/java/ical4j.properties
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
net.fortuna.ical4j.timezone.update.enabled=false
|
||||||
|
|
||||||
|
ical4j.unfolding.relaxed=true
|
||||||
|
ical4j.parsing.relaxed=true
|
||||||
|
ical4j.compatibility.outlook=true
|
BIN
app/src/main/res/drawable-hdpi/addressbook.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/drawable-hdpi/alerts_and_states_warning.png
Normal file
After Width: | Height: | Size: 878 B |
BIN
app/src/main/res/drawable-hdpi/calendar.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-hdpi/extra_actions_about.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_action_new_account.png
Normal file
After Width: | Height: | Size: 962 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_new_event.png
Normal file
After Width: | Height: | Size: 409 B |
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_read_only.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-hdpi/navigation_accept.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/navigation_forward.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-hdpi/show_sync_settings.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-hdpi/view_website.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/drawable-mdpi/addressbook.png
Normal file
After Width: | Height: | Size: 804 B |
BIN
app/src/main/res/drawable-mdpi/alerts_and_states_warning.png
Normal file
After Width: | Height: | Size: 613 B |
BIN
app/src/main/res/drawable-mdpi/calendar.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/drawable-mdpi/extra_actions_about.png
Normal file
After Width: | Height: | Size: 671 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_new_account.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_new_event.png
Normal file
After Width: | Height: | Size: 384 B |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_read_only.png
Normal file
After Width: | Height: | Size: 868 B |