Group support (VCard 3 CATEGORIES) with vcard4android

* VCard 3-style group support (CATEGORIES)
* sync error notification improvements
* some tests
pull/2/head
Ricki Hirner 9 years ago
parent 410a04dc11
commit 4ecca76a95
No known key found for this signature in database
GPG Key ID: C4A212CF0B2B4566

@ -18,7 +18,7 @@ android {
targetSdkVersion 23
versionCode 76
versionName "0.9-alpha4"
versionName "0.9-beta1"
buildConfigField "java.util.Date", "buildTime", "new java.util.Date()"
}

@ -1,78 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import junit.framework.TestCase;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.ExDate;
import net.fortuna.ical4j.model.property.RDate;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
public class DateUtilsTest extends TestCase {
private static final String tzIdVienna = "Europe/Vienna";
public void testRecurrenceSetsToAndroidString() throws ParseException {
// one entry without time zone (implicitly UTC)
final List<RDate> list = new ArrayList<>(2);
list.add(new RDate(new DateList("20150101T103010Z,20150102T103020Z", Value.DATE_TIME)));
assertEquals("20150101T103010Z,20150102T103020Z", DateUtils.recurrenceSetsToAndroidString(list, false));
// two entries (previous one + this), both with time zone Vienna
list.add(new RDate(new DateList("20150103T113030,20150704T123040", Value.DATE_TIME)));
final TimeZone tz = DateUtils.tzRegistry.getTimeZone(tzIdVienna);
for (RDate rdate : list)
rdate.setTimeZone(tz);
assertEquals("20150101T103010Z,20150102T103020Z,20150103T103030Z,20150704T103040Z", DateUtils.recurrenceSetsToAndroidString(list, false));
// DATEs (without time) have to be converted to <date>T000000Z for Android
list.clear();
list.add(new RDate(new DateList("20150101,20150702", Value.DATE)));
assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true));
// DATE-TIME (floating time or UTC) recurrences for all-day events have to converted to <date>T000000Z for Android
list.clear();
list.add(new RDate(new DateList("20150101T000000,20150702T000000Z", Value.DATE_TIME)));
assertEquals("20150101T000000Z,20150702T000000Z", DateUtils.recurrenceSetsToAndroidString(list, true));
}
public void testAndroidStringToRecurrenceSets() throws ParseException {
// list of UTC times
ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, false);
DateList exDates = exDate.getDates();
assertEquals(Value.DATE_TIME, exDates.getType());
assertTrue(exDates.isUtc());
assertEquals(2, exDates.size());
assertEquals(1420108210000L, exDates.get(0).getTime());
assertEquals(1435833020000L, exDates.get(1).getTime());
// list of time zone times
exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(tzIdVienna + ";20150101T103010,20150702T103020", ExDate.class, false);
exDates = exDate.getDates();
assertEquals(Value.DATE_TIME, exDates.getType());
assertEquals(DateUtils.tzRegistry.getTimeZone(tzIdVienna), exDates.getTimeZone());
assertEquals(2, exDates.size());
assertEquals(1420104610000L, exDates.get(0).getTime());
assertEquals(1435825820000L, exDates.get(1).getTime());
// list of dates
exDate = (ExDate)DateUtils.androidStringToRecurrenceSet("20150101T103010Z,20150702T103020Z", ExDate.class, true);
exDates = exDate.getDates();
assertEquals(Value.DATE, exDates.getType());
assertEquals(2, exDates.size());
assertEquals("20150101", exDates.get(0).toString());
assertEquals("20150702", exDates.get(1).toString());
}
}

@ -1,70 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid;
import junit.framework.TestCase;
import java.net.URI;
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.example/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.example:123" + validPath), URIUtils.parseURI("https://www.test.example:123" + validPath, false));
assertEquals(new URI(validPath), URIUtils.parseURI(validPath, true));
// keep literal IPv6 addresses (only in host name)
assertEquals(new URI("https://[1:2::1]/"), URIUtils.parseURI("https://[1:2::1]/", false));
// "~" as home directory (valid)
assertEquals(new URI("http://www.test.example/~user1/"), URIUtils.parseURI("http://www.test.example/~user1/", false));
assertEquals(new URI("/~user1/"), URIUtils.parseURI("/%7euser1/", true));
// "@" in path names (valid)
assertEquals(new URI("http://www.test.example/user@server.com/"), URIUtils.parseURI("http://www.test.example/user@server.com/", false));
assertEquals(new URI("/user@server.com/"), URIUtils.parseURI("/user%40server.com/", true));
assertEquals(new URI("user@server.com"), URIUtils.parseURI("user%40server.com", true));
// ":" in path names (valid)
assertEquals(new URI("http://www.test.example/my:cal.ics"), URIUtils.parseURI("http://www.test.example/my:cal.ics", false));
assertEquals(new URI("/my:cal.ics"), URIUtils.parseURI("/my%3Acal.ics", true));
assertEquals(new URI(null, null, "my:cal.ics", null, null), URIUtils.parseURI("my%3Acal.ics", true));
// common invalid path names
assertEquals(new URI(null, null, "my cal.ics", null, null), URIUtils.parseURI("my cal.ics", true));
assertEquals(new URI(null, null, "{1234}.vcf", null, null), URIUtils.parseURI("{1234}.vcf", true));
}
}

@ -1,117 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.util.TimeZones;
import java.io.IOException;
import java.io.InputStream;
import at.bitfire.davdroid.DateUtils;
import lombok.Cleanup;
public class EventTest extends InstrumentationTestCase {
protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
AssetManager assetMgr;
Event eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec;
public void setUp() throws IOException, InvalidResourceException {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
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");
}
public void testGetTzID() throws Exception {
// DATE (without time)
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new Date("20150101"))));
// DATE-TIME without time zone (floating time): should be UTC (because net.fortuna.ical4j.timezone.date.floating=false)
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime("20150101T000000"))));
// DATE-TIME without time zone (UTC)
assertEquals(TimeZones.UTC_ID, Event.getTzId(new DtStart(new DateTime(1438607288000L))));
// DATE-TIME with time zone
assertEquals(tzVienna.getID(), Event.getTzId(new DtStart(new DateTime("20150101T000000", tzVienna))));
}
public void testRecurringWithException() throws Exception {
Event event = parseCalendar("recurring-with-exception1.ics");
assertTrue(event.isAllDay());
assertEquals(1, event.getExceptions().size());
Event exception = event.getExceptions().get(0);
assertEquals("20150503", exception.recurrenceId.getValue());
assertEquals("Another summary for the third day", exception.summary);
}
public void testStartEndTimes() throws IOException, ParserException, InvalidResourceException {
// event with start+end date-time
Event eViennaEvolution = parseCalendar("vienna-evolution.ics");
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(TimeZones.UTC_ID, eOnThatDay.getDtStartTzID());
// DTEND missing in VEVENT, must have been set to DTSTART+1 day
assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis());
assertEquals(TimeZones.UTC_ID, eOnThatDay.getDtEndTzID());
// event with start+end date for all-day event (one day)
assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtStartTzID());
assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay1Day.getDtEndTzID());
// event with start+end date for all-day event (ten days)
assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtStartTzID());
assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay10Days.getDtEndTzID());
// event with start+end date on some day (invalid 0 sec-event)
assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtStartTzID());
// DTEND invalid in VEVENT, must have been set to DTSTART+1 day
assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis());
assertEquals(TimeZones.UTC_ID, eAllDay0Sec.getDtEndTzID());
}
public void testUnfolding() throws IOException, InvalidResourceException {
Event e = parseCalendar("two-line-description-without-crlf.ics");
assertEquals("http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportcentergroup=&day=6", e.description);
}
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, null, null);
return e;
}
}

@ -1,227 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.Manifest;
import android.accounts.Account;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Calendars;
import android.provider.CalendarContract.Events;
import android.test.InstrumentationTestCase;
import android.util.Log;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.Dur;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.component.VAlarm;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStart;
import java.text.ParseException;
import java.util.Calendar;
import at.bitfire.davdroid.DateUtils;
import lombok.Cleanup;
public class LocalCalendarTest extends InstrumentationTestCase {
private static final String
TAG = "davdroid.test",
accountType = "at.bitfire.davdroid.test",
calendarName = "DAVdroid_Test";
Context targetContext;
ContentProviderClient providerClient;
final Account testAccount = new Account(calendarName, accountType);
Uri calendarURI;
LocalCalendar testCalendar;
// helpers
private Uri syncAdapterURI(Uri uri) {
return uri.buildUpon()
.appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType)
.appendQueryParameter(Calendars.ACCOUNT_NAME, accountType)
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").
build();
}
private long insertNewEvent() throws 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
@Override
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
protected void setUp() throws LocalStorageException, RemoteException {
targetContext = getInstrumentation().getTargetContext();
targetContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_CALENDAR, "No privileges for managing calendars");
providerClient = targetContext.getContentResolver().acquireContentProviderClient(CalendarContract.AUTHORITY);
prepareTestCalendar();
}
private void prepareTestCalendar() throws LocalStorageException, RemoteException {
@Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI, new String[] { Calendars._ID },
Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.ACCOUNT_NAME + "=?",
new String[] { testAccount.type, testAccount.name }, null);
if (cursor != null && cursor.moveToNext())
calendarURI = ContentUris.withAppendedId(Calendars.CONTENT_URI, cursor.getLong(0));
else {
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(ServerInfo.ResourceInfo.Type.CALENDAR, false, null, "Test Calendar", null, null);
calendarURI = LocalCalendar.create(testAccount, targetContext.getContentResolver(), info);
}
Log.i(TAG, "Prepared test calendar " + calendarURI);
testCalendar = new LocalCalendar(testAccount, providerClient, ContentUris.parseId(calendarURI), null);
}
@Override
protected void tearDown() throws RemoteException {
deleteTestCalendar();
}
protected void deleteTestCalendar() throws RemoteException {
Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendar.id);
if (providerClient.delete(uri,null,null)>0)
Log.i(TAG,"Deleted test calendar "+uri);
else
Log.e(TAG,"Couldn't delete test calendar "+uri);
}
// tests
public void testBuildEntry() throws LocalStorageException, ParseException {
final String vcardName = "testBuildEntry";
final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
assertNotNull(tzVienna);
// build and write event to calendar provider
Event event = new Event(vcardName, null);
event.summary = "Sample event";
event.description = "Sample event with date/time";
event.location = "Sample location";
event.dtStart = new DtStart("20150501T120000", tzVienna);
event.dtEnd = new DtEnd("20150501T130000", tzVienna);
assertFalse(event.isAllDay());
// set an alarm one day, two hours, three minutes and four seconds before begin of event
event.getAlarms().add(new VAlarm(new Dur(-1, -2, -3, -4)));
testCalendar.add(event);
testCalendar.commit();
// read and parse event from calendar provider
Event event2 = testCalendar.findByRemoteName(vcardName, true);
assertNotNull("Couldn't build and insert event", event);
// compare with original event
try {
assertEquals(event.summary, event2.summary);
assertEquals(event.description, event2.description);
assertEquals(event.location, event2.location);
assertEquals(event.dtStart, event2.dtStart);
assertFalse(event2.isAllDay());
assertEquals(1, event2.getAlarms().size());
VAlarm alarm = event2.getAlarms().get(0);
assertEquals(event.summary, alarm.getDescription().getValue()); // should be built from event name
assertEquals(new Dur(0, 0, -(24*60 + 60*2 + 3), 0), alarm.getTrigger().getDuration()); // calendar provider stores trigger in minutes
} finally {
testCalendar.delete(event);
}
}
public void testBuildAllDayEntry() throws LocalStorageException, ParseException {
final String vcardName = "testBuildAllDayEntry";
// build and write event to calendar provider
Event event = new Event(vcardName, null);
event.summary = "All-day event";
event.description = "All-day event for testing";
event.location = "Sample location testBuildAllDayEntry";
event.dtStart = new DtStart(new Date("20150501"));
event.dtEnd = new DtEnd(new Date("20150502"));
assertTrue(event.isAllDay());
testCalendar.add(event);
testCalendar.commit();
// read and parse event from calendar provider
Event event2 = testCalendar.findByRemoteName(vcardName, true);
assertNotNull("Couldn't build and insert event", event);
// compare with original event
try {
assertEquals(event.summary, event2.summary);
assertEquals(event.description, event2.description);
assertEquals(event.location, event2.location);
assertEquals(event.dtStart, event2.dtStart);
assertTrue(event2.isAllDay());
} finally {
testCalendar.delete(event);
}
}
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
final 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);
}
}
}

@ -1,95 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import junit.framework.TestCase;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.DtStart;
import java.io.StringReader;
import at.bitfire.davdroid.DateUtils;
public class iCalendarTest extends TestCase {
protected final TimeZone tzVienna = DateUtils.tzRegistry.getTimeZone("Europe/Vienna");
public void testTimezoneDefToTzId() {
// test valid definition
assertEquals("US-Eastern", Event.TimezoneDefToTzId("BEGIN:VCALENDAR\n" +
"PRODID:-//Example Corp.//CalDAV Client//EN\n" +
"VERSION:2.0\n" +
"BEGIN:VTIMEZONE\n" +
"TZID:US-Eastern\n" +
"LAST-MODIFIED:19870101T000000Z\n" +
"BEGIN:STANDARD\n" +
"DTSTART:19671029T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" +
"TZOFFSETFROM:-0400\n" +
"TZOFFSETTO:-0500\n" +
"TZNAME:Eastern Standard Time (US &amp; Canada)\n" +
"END:STANDARD\n" +
"BEGIN:DAYLIGHT\n" +
"DTSTART:19870405T020000\n" +
"RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" +
"TZOFFSETFROM:-0500\n" +
"TZOFFSETTO:-0400\n" +
"TZNAME:Eastern Daylight Time (US &amp; Canada)\n" +
"END:DAYLIGHT\n" +
"END:VTIMEZONE\n" +
"END:VCALENDAR"));
// test invalid time zone
assertNull(iCalendar.TimezoneDefToTzId("/* invalid content */"));
// test time zone without TZID
assertNull(iCalendar.TimezoneDefToTzId("BEGIN:VCALENDAR\n" +
"PRODID:-//Inverse inc./SOGo 2.2.10//EN\n" +
"VERSION:2.0\n" +
"END:VCALENDAR"));
}
public void testValidateTimeZone() throws Exception {
assertNotNull(tzVienna);
// date (no time zone) should be ignored
DtStart date = new DtStart(new Date("20150101"));
iCalendar.validateTimeZone(date);
assertNull(date.getTimeZone());
// date-time (Europe/Vienna) should be unchanged
DtStart dtStart = new DtStart("20150101", tzVienna);
iCalendar.validateTimeZone(dtStart);
assertEquals(tzVienna, dtStart.getTimeZone());
// time zone that is not available on Android systems should be changed to system default
CalendarBuilder builder = new CalendarBuilder();
net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader("BEGIN:VCALENDAR\n" +
"BEGIN:VTIMEZONE\n" +
"TZID:CustomTime\n" +
"BEGIN:STANDARD\n" +
"TZOFFSETFROM:-0400\n" +
"TZOFFSETTO:-0500\n" +
"DTSTART:19600101T000000\n" +
"END:STANDARD\n" +
"END:VTIMEZONE\n" +
"END:VCALENDAR"));
final TimeZone tzCustom = new TimeZone((VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE));
dtStart = new DtStart("20150101T000000", tzCustom);
iCalendar.validateTimeZone(dtStart);
final TimeZone tzDefault = DateUtils.tzRegistry.getTimeZone(java.util.TimeZone.getDefault().getID());
assertNotNull(tzDefault);
assertEquals(tzDefault.getID(), dtStart.getTimeZone().getID());
}
}

@ -8,72 +8,26 @@
package at.bitfire.davdroid.syncadapter;
import android.test.InstrumentationTestCase;
import junit.framework.TestCase;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import at.bitfire.davdroid.TestConstants;
import at.bitfire.davdroid.resource.DavResourceFinder;
import at.bitfire.davdroid.resource.ServerInfo;
import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo;
public class DavResourceFinderTest extends InstrumentationTestCase {
public class DavResourceFinderTest extends TestCase {
DavResourceFinder finder;
@Override
protected void setUp() {
finder = new DavResourceFinder(getInstrumentation().getContext());
}
@Override
protected void tearDown() throws IOException {
}
}
public void testFindResourcesRobohydra() throws Exception {
ServerInfo info = new ServerInfo(new URI(TestConstants.ROBOHYDRA_BASE), "test", "test", true);
finder.findResources(info);
/*** CardDAV ***/
List<ResourceInfo> collections = info.getAddressBooks();
// two address books
assertEquals(2, collections.size());
ResourceInfo collection = collections.get(0);
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default.vcf/").toString(), collection.getURL());
assertEquals("Default Address Book", collection.getDescription());
// second one
collection = collections.get(1);
assertEquals("https://my.server/absolute:uri/my-address-book/", collection.getURL());
assertEquals("Absolute URI VCard Book", collection.getDescription());
/*** CalDAV ***/
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(TestConstants.ROBOHYDRA_BASE), "test", "test", true);
assertEquals(TestConstants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "caldav"));
assertEquals(TestConstants.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"));
}
public void testFindResources() {
// TODO
}
}

@ -1,72 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.webdav;
import android.test.InstrumentationTestCase;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGetHC4;
import org.apache.http.client.methods.HttpPostHC4;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
import java.net.URI;
import at.bitfire.davdroid.TestConstants;
public class DavHttpClientTest extends InstrumentationTestCase {
final static URI testCookieURI = TestConstants.roboHydra.resolve("/dav/testCookieStore");
CloseableHttpClient httpClient;
@Override
protected void setUp() throws Exception {
httpClient = DavHttpClient.create();
}
@Override
protected void tearDown() throws Exception {
httpClient.close();
}
public void testCookies() throws IOException {
CloseableHttpResponse response = null;
HttpGetHC4 get = new HttpGetHC4(testCookieURI);
get.setHeader("Accept", "text/xml");
// at first, DavHttpClient doesn't send a cookie
try {
response = httpClient.execute(get);
assertEquals(412, response.getStatusLine().getStatusCode());
} finally {
if (response != null)
response.close();
}
// POST sets a cookie to DavHttpClient
try {
response = httpClient.execute(new HttpPostHC4(testCookieURI));
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
if (response != null)
response.close();
}
// and now DavHttpClient sends a cookie for GET, too
try {
response = httpClient.execute(get);
assertEquals(200, response.getStatusLine().getStatusCode());
} finally {
if (response != null)
response.close();
}
}
}

@ -1,83 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.webdav;
import junit.framework.TestCase;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import java.io.IOException;
import at.bitfire.davdroid.TestConstants;
public class DavRedirectStrategyTest extends TestCase {
CloseableHttpClient httpClient;
final 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(TestConstants.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(TestConstants.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(TestConstants.roboHydra.resolve(newLocation), redirected.getURI());
}
// error cases
public void testMissingLocation() throws Exception {
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(TestConstants.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(TestConstants.roboHydra.resolve("redirect/relative"));
HttpResponse response = httpClient.execute(request, context);
assertTrue(strategy.isRedirected(request, response, context));
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
assertEquals(TestConstants.roboHydra.resolve("/new/location"), redirected.getURI());
}
}

@ -1,116 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.webdav;
import android.util.Log;
import junit.framework.TestCase;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpHost;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.cert.CertPathValidatorException;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import lombok.Cleanup;
public class TlsSniSocketFactoryTest extends TestCase {
private static final String TAG = "davdroid.TlsSniSocketFactoryTest";
final TlsSniSocketFactory factory = TlsSniSocketFactory.getSocketFactory();
private InetSocketAddress sampleTlsEndpoint;
@Override
protected void setUp() {
// sni.velox.ch is used to test SNI (without SNI support, the certificate is invalid)
sampleTlsEndpoint = new InetSocketAddress("sni.velox.ch", 443);
}
public void testCreateSocket() {
try {
@Cleanup Socket socket = factory.createSocket(null);
assertFalse(socket.isConnected());
} catch (IOException e) {
fail();
}
}
public void testConnectSocket() {
try {
factory.connectSocket(1000, null, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null);
} catch (IOException e) {
Log.e(TAG, "I/O exception", e);
fail();
}
}
public void testCreateLayeredSocket() {
try {
// connect plain socket first
@Cleanup Socket plain = new Socket();
plain.connect(sampleTlsEndpoint);
assertTrue(plain.isConnected());
// then create TLS socket on top of it and establish TLS Connection
@Cleanup Socket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null);
assertTrue(socket.isConnected());
} catch (IOException e) {
Log.e(TAG, "I/O exception", e);
fail();
}
}
public void testProtocolVersions() throws IOException {
String enabledProtocols[] = TlsSniSocketFactory.protocols;
// SSL (all versions) should be disabled
for (String protocol : enabledProtocols)
assertFalse(protocol.contains("SSL"));
// TLS v1+ should be enabled
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1"));
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.1"));
assertTrue(ArrayUtils.contains(enabledProtocols, "TLSv1.2"));
}
public void testHostnameNotInCertificate() throws IOException {
try {
// host with certificate that doesn't match host name
// use the IP address as host name because IP addresses are usually not in the certificate subject
final String ipHostname = sampleTlsEndpoint.getAddress().getHostAddress();
InetSocketAddress host = new InetSocketAddress(ipHostname, 443);
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(ipHostname), host, null, null);
fail();
} catch (SSLException e) {
Log.i(TAG, "Expected exception", e);
assertFalse(ExceptionUtils.indexOfType(e, SSLException.class) == -1);
}
}
public void testUntrustedCertificate() throws IOException {
try {
// host with certificate that is not trusted by default
InetSocketAddress host = new InetSocketAddress("cacert.org", 443);
@Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null);
fail();
} catch (SSLHandshakeException e) {
Log.i(TAG, "Expected exception", e);
assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1);
}
}
}

@ -1,286 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.webdav;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import org.apache.commons.io.IOUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import javax.net.ssl.SSLPeerUnverifiedException;
import at.bitfire.davdroid.TestConstants;
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
import ezvcard.VCardVersion;
import lombok.Cleanup;
// tests require running robohydra!
public class WebDavResourceTest extends InstrumentationTestCase {
final static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
AssetManager assetMgr;
CloseableHttpClient httpClient;
WebDavResource baseDAV;
WebDavResource davAssets,
davCollection, davNonExistingFile, davExistingFile;
@Override
protected void setUp() throws Exception {
httpClient = DavHttpClient.create();
assetMgr = getInstrumentation().getContext().getResources().getAssets();
baseDAV = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("/dav/"));
davAssets = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("assets/"));
davCollection = new WebDavResource(httpClient, TestConstants.roboHydra.resolve("dav/"));
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
}
@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(new URI("/dav/principals/users/test"), davCollection.getProperties().getCurrentUserPrincipal());
WebDavResource simpleFile = new WebDavResource(davAssets, "test.random");
try {
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
fail();
} catch(DavException ex) {
}
assertNull(simpleFile.getProperties().getCurrentUserPrincipal());
}
public void testPropfindHomeSets() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "principals/users/test");
dav.propfind(HttpPropfind.Mode.HOME_SETS);
assertEquals(new URI("/dav/addressbooks/test/"), dav.getProperties().getAddressbookHomeSet());
assertEquals(new URI("/dav/calendars/test/"), dav.getProperties().getCalendarHomeSet());
}
public void testPropfindAddressBooks() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test");
dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS);
// there should be two address books
assertEquals(3, dav.getMembers().size());
// the first one is not an address book and not even a collection (referenced by relative URI)
WebDavResource ab = dav.getMembers().get(0);
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/useless-member"), ab.getLocation());
assertEquals("useless-member", ab.getName());
assertFalse(ab.getProperties().isAddressBook());
// the second one is an address book (referenced by relative URI)
ab = dav.getMembers().get(1);
assertEquals(TestConstants.roboHydra.resolve("/dav/addressbooks/test/default.vcf/"), ab.getLocation());
assertEquals("default.vcf", ab.getName());
assertTrue(ab.getProperties().isAddressBook());
// the third one is an address book (referenced by an absolute URI)
ab = dav.getMembers().get(2);
assertEquals(new URI("https://my.server/absolute:uri/my-address-book/"), ab.getLocation());
assertEquals("my-address-book", ab.getName());
assertTrue(ab.getProperties().isAddressBook());
}
public void testPropfindCalendars() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
dav.propfind(Mode.CALDAV_COLLECTIONS);
assertEquals(3, dav.getMembers().size());
assertEquals(new Integer(0xFFFF00FF), dav.getMembers().get(2).getProperties().getColor());
for (WebDavResource member : dav.getMembers()) {
if (member.getName().contains(".ics"))
assertTrue(member.getProperties().isCalendar());
else
assertFalse(member.getProperties().isCalendar());
assertFalse(member.getProperties().isAddressBook());
}
}
public void testPropfindCollectionProperties() throws Exception {
WebDavResource dav = new WebDavResource(davCollection, "propfind-collection-properties");
dav.propfind(Mode.COLLECTION_PROPERTIES);
assertTrue(dav.members.isEmpty());
assertTrue(dav.properties.isCollection);
assertTrue(dav.properties.isAddressBook);
assertNull(dav.properties.displayName);
assertNull(dav.properties.color);
assertEquals(VCardVersion.V4_0, dav.properties.supportedVCardVersion);
}
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, new URI(path));
davSlash.propfind(Mode.CARDDAV_COLLECTIONS);
assertEquals(new URI(principalOK), davSlash.getProperties().getCurrentUserPrincipal());
}
}
public void testStrangeMemberNames() throws Exception {
// construct a WebDavResource from a base collection and a member which is an encoded URL (see https://github.com/bitfireAT/davdroid/issues/482)
WebDavResource dav = new WebDavResource(davCollection, "http%3A%2F%2Fwww.invalid.example%2Fm8%2Ffeeds%2Fcontacts%2Fmaria.mueller%2540gmail.com%2Fbase%2F5528abc5720cecc.vcf");
dav.get("text/vcard");
}
/* test normal HTTP/WebDAV */
public void testPropfindRedirection() throws Exception {
// PROPFIND redirection
WebDavResource redirected = new WebDavResource(baseDAV, new URI("/redirect/301?to=/dav/"));
redirected.propfind(Mode.CURRENT_USER_PRINCIPAL);
assertEquals("/dav/", redirected.getLocation().getPath());
}
public void testGet() throws Exception {
WebDavResource simpleFile = new WebDavResource(davAssets, "test.random");
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:3@my%40pc.vcf" });
// queried address book has a name
assertEquals("My Book", davAddressBook.getProperties().getDisplayName());
// there are two contacts
assertEquals(2, davAddressBook.getMembers().size());
// contact file names should be unescaped (yes, it's really named ...%40pc... to check double-encoding)
assertEquals("1.vcf", davAddressBook.getMembers().get(0).getName());
assertEquals("2:3@my%40pc.vcf", davAddressBook.getMembers().get(1).getName());
// all contacts have some content
for (WebDavResource member : davAddressBook.getMembers())
assertNotNull(member.getContent());
}
public void testMultiGetWith404() throws Exception {
WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default-with-404.vcf/");
try {
davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[]{ "notexisting" });
fail();
} catch(NotFoundException e) {
// addressbooks/default.vcf/notexisting doesn't exist,
// so server responds with 404 which causes a NotFoundException
}
}
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 (resource has been deleted on server, thus server returns 412)
try {
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
fail();
} catch(PreconditionFailedException ex) {
}
// should fail on existing file with wrong ETag (resource has changed on server, thus server returns 409)
try {
WebDavResource dav = new WebDavResource(davCollection, new URI("collection/existing.file?conflict=1"));
dav.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
fail();
} catch(ConflictException 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 testGetSpecialURLs() throws Exception {
WebDavResource dav = new WebDavResource(davAssets, "member-with:colon.vcf");
try {
dav.get("*/*");
fail();
} catch(NotFoundException e) {
assertTrue(true);
}
}
}

@ -9,19 +9,28 @@ package at.bitfire.davdroid.resource;
import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import java.security.acl.Group;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.davdroid.Constants;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidContactFactory;
import at.bitfire.vcard4android.AndroidGroupFactory;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import lombok.Synchronized;
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
@ -51,7 +60,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
*/
@Override
public LocalContact[] getDeleted() throws ContactsStorageException {
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + "!=0", null);
return (LocalContact[])queryContacts(RawContacts.DELETED + "!=0", null);
}
/**
@ -59,7 +68,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
*/
@Override
public LocalContact[] getDirty() throws ContactsStorageException {
return (LocalContact[])queryContacts(ContactsContract.RawContacts.DIRTY + "!=0", null);
return (LocalContact[])queryContacts(RawContacts.DIRTY + "!=0", null);
}
/**
@ -71,6 +80,76 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
}
// GROUPS
/**
* Finds the first group with the given title.
* @param displayName title of the group to look for
* @return group with given title, or null if none
*/
public LocalGroup findGroupByTitle(String displayName) throws ContactsStorageException {
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
ContactsContract.Groups.TITLE + "=?", new String[] { displayName }, null);
if (cursor != null && cursor.moveToNext())
return new LocalGroup(this, cursor.getLong(0));
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't find local contact group", e);
}
return null;
}
public LocalGroup[] getDeletedGroups() throws ContactsStorageException {
List<LocalGroup> groups = new LinkedList<>();
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
Groups.DELETED + "!=0", null, null);
while (cursor != null && cursor.moveToNext())
groups.add(new LocalGroup(this, cursor.getLong(0)));
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query deleted groups", e);
}
return groups.toArray(new LocalGroup[groups.size()]);
}
public LocalGroup[] getDirtyGroups() throws ContactsStorageException {
List<LocalGroup> groups = new LinkedList<>();
try {
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID },
Groups.DIRTY + "!=0", null, null);
while (cursor != null && cursor.moveToNext())
groups.add(new LocalGroup(this, cursor.getLong(0)));
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query dirty groups", e);
}
return groups.toArray(new LocalGroup[groups.size()]);
}
public void markMembersDirty(long groupId) throws ContactsStorageException {
ContentValues dirty = new ContentValues(1);
dirty.put(RawContacts.DIRTY, 1);
try {
// query all GroupMemberships of this groupId, mark every corresponding raw contact as DIRTY
@Cleanup Cursor cursor = provider.query(syncAdapterURI(Data.CONTENT_URI),
new String[] { GroupMembership.RAW_CONTACT_ID },
Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
while (cursor != null && cursor.moveToNext()) {
long id = cursor.getLong(0);
Constants.log.debug("Marking raw contact #" + id + " as dirty");
provider.update(syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)), dirty, null, null);
}
} catch (RemoteException e) {
throw new ContactsStorageException("Couldn't query dirty groups", e);
}
}
// SYNC STATE
protected void readSyncState() throws ContactsStorageException {
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
byte[] raw = getSyncState();

@ -8,14 +8,20 @@
package at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import java.io.FileNotFoundException;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.Constants;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidContactFactory;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import ezvcard.Ezvcard;
@ -62,6 +68,58 @@ public class LocalContact extends AndroidContact implements LocalResource {
}
// group support
@Override
protected void populateGroupMembership(ContentValues row) {
if (row.containsKey(GroupMembership.GROUP_ROW_ID)) {
long groupId = row.getAsLong(GroupMembership.GROUP_ROW_ID);
// fetch group
LocalGroup group = new LocalGroup(addressBook, groupId);
try {
Contact groupInfo = group.getContact();
// add to CATEGORIES
contact.getCategories().add(groupInfo.displayName);
} catch (FileNotFoundException|ContactsStorageException e) {
Constants.log.warn("Couldn't find assigned group #" + groupId + ", ignoring membership", e);
}
}
}
@Override
protected void insertGroupMemberships(BatchOperation batch) throws ContactsStorageException {
for (String category : contact.getCategories()) {
// Is there already a category with this display name?
LocalGroup group = ((LocalAddressBook)addressBook).findGroupByTitle(category);
if (group == null) {
// no, we have to create the group before inserting the membership
Contact groupInfo = new Contact();
groupInfo.displayName = category;
group = new LocalGroup(addressBook, groupInfo);
group.create();
}
Long groupId = group.getId();
if (groupId != null) {
ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(dataSyncURI());
if (id == null)
builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
else
builder.withValue(GroupMembership.RAW_CONTACT_ID, id);
builder .withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
.withValue(GroupMembership.GROUP_ROW_ID, groupId);
batch.enqueue(builder.build());
}
}
}
// factory
static class Factory extends AndroidContactFactory {
static final Factory INSTANCE = new Factory();

@ -0,0 +1,38 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
import android.content.ContentUris;
import android.content.ContentValues;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidGroup;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
public class LocalGroup extends AndroidGroup {
public LocalGroup(AndroidAddressBook addressBook, long id) {
super(addressBook, id);
}
public LocalGroup(AndroidAddressBook addressBook, Contact contact) {
super(addressBook, contact);
}
public void clearDirty() throws ContactsStorageException {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.Groups.DIRTY, 0);
update(values);
}
}

@ -44,6 +44,7 @@ import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalEvent;
import at.bitfire.davdroid.resource.LocalResource;
@ -63,6 +64,10 @@ public class CalendarSyncManager extends SyncManager {
localCollection = calendar;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_calendar, account.name);
}
@Override
protected void prepare() {

@ -23,7 +23,6 @@ import com.squareup.okhttp.ResponseBody;
import org.apache.commons.codec.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -45,8 +44,10 @@ import at.bitfire.dav4android.property.SupportedAddressData;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalGroup;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
@ -67,6 +68,11 @@ public class ContactsSyncManager extends SyncManager {
this.provider = provider;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_contacts, account.name);
}
@Override
protected void prepare() throws ContactsStorageException {
@ -78,6 +84,8 @@ public class ContactsSyncManager extends SyncManager {
throw new ContactsStorageException("Couldn't get address book URL");
collectionURL = HttpUrl.parse(url);
davCollection = new DavAddressBook(httpClient, collectionURL);
processChangedGroups();
}
@Override
@ -185,6 +193,28 @@ public class ContactsSyncManager extends SyncManager {
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
private void processChangedGroups() throws ContactsStorageException {
LocalAddressBook addressBook = localAddressBook();
// groups with DELETED=1: remove group finally
for (LocalGroup group : addressBook.getDeletedGroups()) {
long groupId = group.getId();
Constants.log.debug("Finally removing group #" + groupId);
// remove group memberships, but not as sync adapter (should marks contacts as DIRTY)
// NOTE: doesn't work that way because Contact Provider removes the group memberships even for DELETED groups
// addressBook.removeGroupMemberships(groupId, false);
group.delete();
}
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
for (LocalGroup group : addressBook.getDirtyGroups()) {
long groupId = group.getId();
Constants.log.debug("Marking members of modified group #" + groupId + " as dirty");
addressBook.markMembersDirty(groupId);
group.clearDirty();
}
}
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
if (contacts != null && contacts.length == 1) {

@ -107,6 +107,8 @@ abstract public class SyncManager {
notificationManager.cancel(account.name, this.notificationId = notificationId);
}
protected abstract String getSyncErrorTitle();
public void performSync() {
int syncPhase = SYNC_PHASE_PREPARE;
try {
@ -201,8 +203,8 @@ abstract public class SyncManager {
Notification.Builder builder = new Notification.Builder(context);
Notification notification;
builder .setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(context.getString(R.string.sync_error_title, account.name))
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT));
.setContentTitle(getSyncErrorTitle())
.setContentIntent(PendingIntent.getActivity(context, notificationId, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
if (Build.VERSION.SDK_INT >= 20)
builder.setLocalOnly(true);

@ -44,6 +44,7 @@ import at.bitfire.dav4android.property.GetContentType;
import at.bitfire.dav4android.property.GetETag;
import at.bitfire.davdroid.ArrayUtils;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.davdroid.resource.LocalTask;
@ -67,6 +68,11 @@ public class TasksSyncManager extends SyncManager {
localCollection = taskList;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_tasks, account.name);
}
@Override
protected void prepare() {

@ -187,6 +187,6 @@
<string name="setup_account_name_info">"Použijte svou emailovou adresu jako jméno účtu. Android bude používat tuto hodnotu jako jméno ORGANIZÁTORA událostí které vytvoříte. Nelze mít dva účty se stejným jménem.</string>
<string name="setup_read_only">pouze pro čtení</string>
<string name="sync_error_title">Synchronizace selhala</string>
<string name="sync_error_calendar">Synchronizace selhala</string>
</resources>

@ -182,7 +182,9 @@
<string name="setup_account_name_info">&quot;Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen.</string>
<string name="setup_read_only">schreibgeschützt</string>
<string name="sync_error_title">Synchronisierung von %s fehlgeschlagen</string>
<string name="sync_error_calendar">Kalender-Synchronisierung fehlgeschlagen (%s)</string>
<string name="sync_error_contacts">Adressbuch-Synchronisierung fehlgeschlagen (%s)</string>
<string name="sync_error_tasks">Aufgaben-Synchronisierung fehlgeschlagen (%s)</string>
<string name="sync_error">Fehler beim %s</string>
<string name="sync_error_http_dav">Serverfehler beim %s</string>
<string name="sync_error_local_storage">Datenbank-Fehler beim %s</string>

@ -106,5 +106,5 @@ DAVdroid JB Workaround</a>.</strong></p>
<string name="setup_organizer_hint">"ORGANIZADOR de tus eventos; se necesita si se usa información de los asistentes"</string>
<string name="setup_account_name_info">"Usa tu dirección de correo electrónico como nombre de cuenta porque Android usará el nombre de cuenta como campo de ORGANIZADOR para los eventos que crees. No puedes tener dos cuentas con el mismo nombre.</string>
<string name="setup_read_only">solo lectura</string>
<string name="sync_error_title">La sincronización ha fallado</string>
<string name="sync_error_calendar">La sincronización ha fallado</string>
</resources>

@ -151,7 +151,7 @@
<string name="setup_select_task_lists">Choisir les listes de tâches à synchroniser :</string>
<string name="setup_task_lists">Listes de tâches</string>
<string name="skip">Sauter</string>
<string name="sync_error_title">La synchronisation a échoué</string>
<string name="sync_error_calendar">La synchronisation a échoué</string>
<string name="exception_cert_path_validation">Un certificate dans le chemin n\'est pas sûr. Lire la FAQ pour plus d\'information.</string>
</resources>

@ -125,6 +125,6 @@
<string name="setup_account_name_info">"Gebruik uw e-mailadres als accountnaam omdat Android de accountnaam zal gebruiken als ORGANIZER veld voor afspraken die u maakt. Je kunt geen twee accounts met dezelfde naam gebruiken.</string>
<string name="setup_read_only">Alleen lezen</string>
<string name="sync_error_title">Synchronisatie fout</string>
<string name="sync_error_calendar">Synchronisatie fout</string>
</resources>

@ -174,6 +174,6 @@
<string name="setup_account_name_info">"Używaj adresu e-mail jako nazwy konta, ponieważ Android będzie używał nazwy konta do pola ORGANIZATORA dla wydarzeń stworzonych przez ciebie. Nie możesz posidać dwóch konta z taką samą nazwą.</string>
<string name="setup_read_only">tylko do odczytu</string>
<string name="sync_error_title">Synchronizacja nie powiodła się</string>
<string name="sync_error_calendar">Synchronizacja nie powiodła się</string>
</resources>

@ -113,5 +113,5 @@
<string name="setup_organizer_hint">"ORGANIZADOR de seus evento; requerido se você usa informações dos participantes"</string>
<string name="setup_account_name_info">"Use seu endereço de e-mail como nome da conta porque o Android irá usar esta informação com ORGANIZADOR para eventos criados. Você não pode ter duas contas com o mesmo nome.</string>
<string name="setup_read_only">apenas leitura</string>
<string name="sync_error_title">Falha na sincronização</string>
<string name="sync_error_calendar">Falha na sincronização</string>
</resources>

@ -123,6 +123,6 @@
<string name="setup_account_name_info">"Используйте свой адрес email как имя аккаунта, так как Android будет использовать это имя как поле ОРГАНИЗАТОР для событий которые вы будете создавать. Вы не можете использовать одинаковое имя для разных аккаунтов.</string>
<string name="setup_read_only">только чтение</string>
<string name="sync_error_title">Ошибка синхронизации</string>
<string name="sync_error_calendar">Ошибка синхронизации</string>
</resources>

@ -130,6 +130,6 @@
<string name="setup_account_name_info">"Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива.</string>
<string name="setup_read_only">само-за-читање</string>
<string name="sync_error_title">Синхронизација није успела</string>
<string name="sync_error_calendar">Синхронизација није успела</string>
</resources>

@ -134,6 +134,6 @@
<string name="setup_account_name_info">"请使用您的 E-mail 地址作为账户名,因为 Android 会将帐户名用于您创建的日程的参与者 (ORGANIZER) 项。您不能有两个重名的账户。</string>
<string name="setup_read_only">只读</string>
<string name="sync_error_title">同步失败</string>
<string name="sync_error_calendar">同步失败</string>
</resources>

@ -197,7 +197,9 @@
<!-- sync errors and DebugInfoActivity -->
<string name="debug_info_title">Debug info</string>
<string name="sync_error_title">Synchronization of %s failed</string>
<string name="sync_error_calendar">Calendar synchronization failed (%s)</string>
<string name="sync_error_contacts">Address book synchronization failed (%s)</string>
<string name="sync_error_tasks">Task synchronization failed (%s)</string>
<string name="sync_error">Error while %s</string>
<string name="sync_error_http_dav">Server error while %s</string>
<string name="sync_error_local_storage">Database error while %s</string>

@ -1 +1 @@
Subproject commit 9f722654e355e6e82b8b6e39a515b250feb111a9
Subproject commit 2083d075d3b4a4b9ac0a930af1d019547d7dcf07

@ -1 +1 @@
Subproject commit 662c48c40ad66e9a77f4f4792587dc04ecc4a74e
Subproject commit f2e48b97281064e27197953c178e8613a9413ed7

@ -1 +1 @@
Subproject commit 18b26fe48896b2683b1e04e8df4464f5ceacf8f3
Subproject commit 38a01b7f18a81bb46ee33e2bae62a20d1239c17b
Loading…
Cancel
Save