mirror of
https://github.com/etesync/android
synced 2024-11-23 00:18:19 +00:00
Group support (VCard 3 CATEGORIES) with vcard4android
* VCard 3-style group support (CATEGORIES) * sync error notification improvements * some tests
This commit is contained in:
parent
410a04dc11
commit
4ecca76a95
@ -18,7 +18,7 @@ android {
|
|||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
|
|
||||||
versionCode 76
|
versionCode 76
|
||||||
versionName "0.9-alpha4"
|
versionName "0.9-beta1"
|
||||||
|
|
||||||
buildConfigField "java.util.Date", "buildTime", "new java.util.Date()"
|
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 & 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"));
|
|
||||||
|
|
||||||
// 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;
|
package at.bitfire.davdroid.syncadapter;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
import java.io.IOException;
|
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.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;
|
DavResourceFinder finder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() {
|
protected void setUp() {
|
||||||
finder = new DavResourceFinder(getInstrumentation().getContext());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void tearDown() throws IOException {
|
protected void tearDown() throws IOException {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testFindResources() {
|
||||||
public void testFindResourcesRobohydra() throws Exception {
|
// TODO
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
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.davdroid.Constants;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidContact;
|
import at.bitfire.vcard4android.AndroidContact;
|
||||||
import at.bitfire.vcard4android.AndroidContactFactory;
|
|
||||||
import at.bitfire.vcard4android.AndroidGroupFactory;
|
import at.bitfire.vcard4android.AndroidGroupFactory;
|
||||||
import at.bitfire.vcard4android.Contact;
|
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import lombok.Cleanup;
|
import lombok.Cleanup;
|
||||||
import lombok.Synchronized;
|
|
||||||
|
|
||||||
|
|
||||||
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
|
public class LocalAddressBook extends AndroidAddressBook implements LocalCollection {
|
||||||
@ -51,7 +60,7 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LocalContact[] getDeleted() throws ContactsStorageException {
|
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
|
@Override
|
||||||
public LocalContact[] getDirty() throws ContactsStorageException {
|
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 {
|
protected void readSyncState() throws ContactsStorageException {
|
||||||
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
|
||||||
byte[] raw = getSyncState();
|
byte[] raw = getSyncState();
|
||||||
|
@ -8,14 +8,20 @@
|
|||||||
|
|
||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
import android.content.ContentProviderOperation;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
|
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
import at.bitfire.davdroid.BuildConfig;
|
import at.bitfire.davdroid.BuildConfig;
|
||||||
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.vcard4android.AndroidAddressBook;
|
import at.bitfire.vcard4android.AndroidAddressBook;
|
||||||
import at.bitfire.vcard4android.AndroidContact;
|
import at.bitfire.vcard4android.AndroidContact;
|
||||||
import at.bitfire.vcard4android.AndroidContactFactory;
|
import at.bitfire.vcard4android.AndroidContactFactory;
|
||||||
|
import at.bitfire.vcard4android.BatchOperation;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
import ezvcard.Ezvcard;
|
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 class Factory extends AndroidContactFactory {
|
||||||
static final Factory INSTANCE = new Factory();
|
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.dav4android.property.GetETag;
|
||||||
import at.bitfire.davdroid.ArrayUtils;
|
import at.bitfire.davdroid.ArrayUtils;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
import at.bitfire.davdroid.resource.LocalEvent;
|
import at.bitfire.davdroid.resource.LocalEvent;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
@ -63,6 +64,10 @@ public class CalendarSyncManager extends SyncManager {
|
|||||||
localCollection = calendar;
|
localCollection = calendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSyncErrorTitle() {
|
||||||
|
return context.getString(R.string.sync_error_calendar, account.name);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() {
|
protected void prepare() {
|
||||||
|
@ -23,7 +23,6 @@ import com.squareup.okhttp.ResponseBody;
|
|||||||
|
|
||||||
import org.apache.commons.codec.Charsets;
|
import org.apache.commons.codec.Charsets;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -45,8 +44,10 @@ import at.bitfire.dav4android.property.SupportedAddressData;
|
|||||||
import at.bitfire.davdroid.ArrayUtils;
|
import at.bitfire.davdroid.ArrayUtils;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
import at.bitfire.davdroid.HttpClient;
|
import at.bitfire.davdroid.HttpClient;
|
||||||
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.resource.LocalAddressBook;
|
import at.bitfire.davdroid.resource.LocalAddressBook;
|
||||||
import at.bitfire.davdroid.resource.LocalContact;
|
import at.bitfire.davdroid.resource.LocalContact;
|
||||||
|
import at.bitfire.davdroid.resource.LocalGroup;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.vcard4android.Contact;
|
import at.bitfire.vcard4android.Contact;
|
||||||
import at.bitfire.vcard4android.ContactsStorageException;
|
import at.bitfire.vcard4android.ContactsStorageException;
|
||||||
@ -67,6 +68,11 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSyncErrorTitle() {
|
||||||
|
return context.getString(R.string.sync_error_contacts, account.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() throws ContactsStorageException {
|
protected void prepare() throws ContactsStorageException {
|
||||||
@ -78,6 +84,8 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
throw new ContactsStorageException("Couldn't get address book URL");
|
throw new ContactsStorageException("Couldn't get address book URL");
|
||||||
collectionURL = HttpUrl.parse(url);
|
collectionURL = HttpUrl.parse(url);
|
||||||
davCollection = new DavAddressBook(httpClient, collectionURL);
|
davCollection = new DavAddressBook(httpClient, collectionURL);
|
||||||
|
|
||||||
|
processChangedGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -185,6 +193,28 @@ public class ContactsSyncManager extends SyncManager {
|
|||||||
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
||||||
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
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 {
|
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
||||||
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
||||||
if (contacts != null && contacts.length == 1) {
|
if (contacts != null && contacts.length == 1) {
|
||||||
|
@ -107,6 +107,8 @@ abstract public class SyncManager {
|
|||||||
notificationManager.cancel(account.name, this.notificationId = notificationId);
|
notificationManager.cancel(account.name, this.notificationId = notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract String getSyncErrorTitle();
|
||||||
|
|
||||||
public void performSync() {
|
public void performSync() {
|
||||||
int syncPhase = SYNC_PHASE_PREPARE;
|
int syncPhase = SYNC_PHASE_PREPARE;
|
||||||
try {
|
try {
|
||||||
@ -201,8 +203,8 @@ abstract public class SyncManager {
|
|||||||
Notification.Builder builder = new Notification.Builder(context);
|
Notification.Builder builder = new Notification.Builder(context);
|
||||||
Notification notification;
|
Notification notification;
|
||||||
builder .setSmallIcon(R.drawable.ic_launcher)
|
builder .setSmallIcon(R.drawable.ic_launcher)
|
||||||
.setContentTitle(context.getString(R.string.sync_error_title, account.name))
|
.setContentTitle(getSyncErrorTitle())
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, detailsIntent, PendingIntent.FLAG_CANCEL_CURRENT));
|
.setContentIntent(PendingIntent.getActivity(context, notificationId, detailsIntent, PendingIntent.FLAG_UPDATE_CURRENT));
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 20)
|
if (Build.VERSION.SDK_INT >= 20)
|
||||||
builder.setLocalOnly(true);
|
builder.setLocalOnly(true);
|
||||||
|
@ -44,6 +44,7 @@ import at.bitfire.dav4android.property.GetContentType;
|
|||||||
import at.bitfire.dav4android.property.GetETag;
|
import at.bitfire.dav4android.property.GetETag;
|
||||||
import at.bitfire.davdroid.ArrayUtils;
|
import at.bitfire.davdroid.ArrayUtils;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
|
import at.bitfire.davdroid.R;
|
||||||
import at.bitfire.davdroid.resource.LocalCalendar;
|
import at.bitfire.davdroid.resource.LocalCalendar;
|
||||||
import at.bitfire.davdroid.resource.LocalResource;
|
import at.bitfire.davdroid.resource.LocalResource;
|
||||||
import at.bitfire.davdroid.resource.LocalTask;
|
import at.bitfire.davdroid.resource.LocalTask;
|
||||||
@ -67,6 +68,11 @@ public class TasksSyncManager extends SyncManager {
|
|||||||
localCollection = taskList;
|
localCollection = taskList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSyncErrorTitle() {
|
||||||
|
return context.getString(R.string.sync_error_tasks, account.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepare() {
|
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_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="setup_read_only">pouze pro čtení</string>
|
||||||
|
|
||||||
<string name="sync_error_title">Synchronizace selhala</string>
|
<string name="sync_error_calendar">Synchronizace selhala</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -182,7 +182,9 @@
|
|||||||
<string name="setup_account_name_info">"Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen.</string>
|
<string name="setup_account_name_info">"Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen.</string>
|
||||||
<string name="setup_read_only">schreibgeschützt</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">Fehler beim %s</string>
|
||||||
<string name="sync_error_http_dav">Serverfehler beim %s</string>
|
<string name="sync_error_http_dav">Serverfehler beim %s</string>
|
||||||
<string name="sync_error_local_storage">Datenbank-Fehler 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_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_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="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>
|
</resources>
|
||||||
|
@ -151,7 +151,7 @@
|
|||||||
<string name="setup_select_task_lists">Choisir les listes de tâches à synchroniser :</string>
|
<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="setup_task_lists">Listes de tâches</string>
|
||||||
<string name="skip">Sauter</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>
|
<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>
|
</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_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="setup_read_only">Alleen lezen</string>
|
||||||
|
|
||||||
<string name="sync_error_title">Synchronisatie fout</string>
|
<string name="sync_error_calendar">Synchronisatie fout</string>
|
||||||
|
|
||||||
</resources>
|
</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_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="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>
|
</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_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_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="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>
|
</resources>
|
||||||
|
@ -123,6 +123,6 @@
|
|||||||
<string name="setup_account_name_info">"Используйте свой адрес email как имя аккаунта, так как Android будет использовать это имя как поле ОРГАНИЗАТОР для событий которые вы будете создавать. Вы не можете использовать одинаковое имя для разных аккаунтов.</string>
|
<string name="setup_account_name_info">"Используйте свой адрес email как имя аккаунта, так как Android будет использовать это имя как поле ОРГАНИЗАТОР для событий которые вы будете создавать. Вы не можете использовать одинаковое имя для разных аккаунтов.</string>
|
||||||
<string name="setup_read_only">только чтение</string>
|
<string name="setup_read_only">только чтение</string>
|
||||||
|
|
||||||
<string name="sync_error_title">Ошибка синхронизации</string>
|
<string name="sync_error_calendar">Ошибка синхронизации</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -130,6 +130,6 @@
|
|||||||
<string name="setup_account_name_info">"Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива.</string>
|
<string name="setup_account_name_info">"Користите вашу е-адресу за назив налога јер Андроид користи назив налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог назива.</string>
|
||||||
<string name="setup_read_only">само-за-читање</string>
|
<string name="setup_read_only">само-за-читање</string>
|
||||||
|
|
||||||
<string name="sync_error_title">Синхронизација није успела</string>
|
<string name="sync_error_calendar">Синхронизација није успела</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -134,6 +134,6 @@
|
|||||||
<string name="setup_account_name_info">"请使用您的 E-mail 地址作为账户名,因为 Android 会将帐户名用于您创建的日程的参与者 (ORGANIZER) 项。您不能有两个重名的账户。</string>
|
<string name="setup_account_name_info">"请使用您的 E-mail 地址作为账户名,因为 Android 会将帐户名用于您创建的日程的参与者 (ORGANIZER) 项。您不能有两个重名的账户。</string>
|
||||||
<string name="setup_read_only">只读</string>
|
<string name="setup_read_only">只读</string>
|
||||||
|
|
||||||
<string name="sync_error_title">同步失败</string>
|
<string name="sync_error_calendar">同步失败</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -197,7 +197,9 @@
|
|||||||
|
|
||||||
<!-- sync errors and DebugInfoActivity -->
|
<!-- sync errors and DebugInfoActivity -->
|
||||||
<string name="debug_info_title">Debug info</string>
|
<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">Error while %s</string>
|
||||||
<string name="sync_error_http_dav">Server 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>
|
<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…
Reference in New Issue
Block a user