Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a68717de authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Group support (VCard 3 CATEGORIES) with vcard4android

* VCard 3-style group support (CATEGORIES)
* sync error notification improvements
* some tests
parent 2f87b51f
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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()"
    }
+0 −78
Original line number Diff line number Diff line
/*
 * 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());
	}

}
+0 −70
Original line number Diff line number Diff line
/*
 * 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));
	}
}
+0 −117
Original line number Diff line number Diff line
/*
 * 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;
	}
}
+0 −227
Original line number Diff line number Diff line
/*
 * 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);
		}
	}

}
Loading