Refactoring
* move common code to super-classes * new icons
Before Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
@ -8,66 +8,30 @@
|
|||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
import net.fortuna.ical4j.data.ParserException;
|
|
||||||
|
|
||||||
import org.apache.http.HttpException;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.webdav.WebDavCollection;
|
|
||||||
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
|
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
|
||||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
|
||||||
|
|
||||||
public class CalDavCalendar extends RemoteCollection {
|
public class CalDavCalendar extends RemoteCollection<Event> {
|
||||||
//private final static String TAG = "davdroid.CalDavCalendar";
|
//private final static String TAG = "davdroid.CalDavCalendar";
|
||||||
|
|
||||||
public CalDavCalendar(String baseURL, String user, String password) throws IOException {
|
|
||||||
try {
|
|
||||||
collection = new WebDavCollection(new URI(baseURL), user, password);
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String memberContentType() {
|
protected String memberContentType() {
|
||||||
return "text/calendar";
|
return "text/calendar";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Event[] getMemberETags() throws IOException, IncapableResourceException, HttpException {
|
protected MultigetType multiGetType() {
|
||||||
super.getMemberETags();
|
return MultigetType.CALENDAR;
|
||||||
|
|
||||||
LinkedList<Event> resources = new LinkedList<Event>();
|
|
||||||
for (WebDavResource member : collection.getMembers()) {
|
|
||||||
Event e = new Event(member.getName(), member.getETag());
|
|
||||||
resources.add(e);
|
|
||||||
}
|
|
||||||
return resources.toArray(new Event[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Event[] multiGet(Resource[] resources) throws IOException, IncapableResourceException, HttpException, ParserException {
|
protected Event newResourceSkeleton(String name, String ETag) {
|
||||||
if (resources.length == 1)
|
return new Event(name, ETag);
|
||||||
return new Event[] { (Event)get(resources[0]) };
|
|
||||||
|
|
||||||
LinkedList<String> names = new LinkedList<String>();
|
|
||||||
for (Resource c : resources)
|
|
||||||
names.add(c.getName());
|
|
||||||
|
|
||||||
collection.multiGet(names.toArray(new String[0]), MultigetType.CALENDAR);
|
|
||||||
|
|
||||||
LinkedList<Event> foundEvents = new LinkedList<Event>();
|
|
||||||
for (WebDavResource member : collection.getMembers()) {
|
|
||||||
Event e = new Event(member.getName(), member.getETag());
|
|
||||||
e.parseEntity(member.getContent());
|
|
||||||
foundEvents.add(e);
|
|
||||||
}
|
|
||||||
return foundEvents.toArray(new Event[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CalDavCalendar(String baseURL, String user, String password) throws IOException, URISyntaxException {
|
||||||
|
super(baseURL, user, password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,70 +8,30 @@
|
|||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
import net.fortuna.ical4j.data.ParserException;
|
|
||||||
|
|
||||||
import org.apache.http.HttpException;
|
|
||||||
|
|
||||||
import at.bitfire.davdroid.webdav.WebDavCollection;
|
|
||||||
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
|
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
|
||||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
|
||||||
|
|
||||||
public class CardDavAddressBook extends RemoteCollection {
|
public class CardDavAddressBook extends RemoteCollection<Contact> {
|
||||||
//private final static String TAG = "davdroid.CardDavAddressBook";
|
//private final static String TAG = "davdroid.CardDavAddressBook";
|
||||||
|
|
||||||
public CardDavAddressBook(String baseURL, String user, String password) throws IOException {
|
|
||||||
try {
|
|
||||||
collection = new WebDavCollection(new URI(baseURL), user, password);
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String memberContentType() {
|
protected String memberContentType() {
|
||||||
return "text/vcard";
|
return "text/vcard";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Contact[] getMemberETags() throws IOException, IncapableResourceException, HttpException {
|
protected MultigetType multiGetType() {
|
||||||
super.getMemberETags();
|
return MultigetType.ADDRESS_BOOK;
|
||||||
|
|
||||||
LinkedList<Contact> resources = new LinkedList<Contact>();
|
|
||||||
for (WebDavResource member : collection.getMembers()) {
|
|
||||||
Contact c = new Contact(member.getName(), member.getETag());
|
|
||||||
resources.add(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resources.toArray(new Contact[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Contact[] multiGet(Resource[] resources) throws IOException, IncapableResourceException, HttpException, ParserException {
|
protected Contact newResourceSkeleton(String name, String ETag) {
|
||||||
if (resources.length == 1) {
|
return new Contact(name, ETag);
|
||||||
Resource resource = get(resources[0]);
|
|
||||||
if (resource != null)
|
|
||||||
return new Contact[] { (Contact)resource };
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkedList<String> names = new LinkedList<String>();
|
|
||||||
for (Resource c : resources)
|
|
||||||
names.add(c.getName());
|
|
||||||
|
|
||||||
collection.multiGet(names.toArray(new String[0]), MultigetType.ADDRESS_BOOK);
|
|
||||||
|
|
||||||
LinkedList<Contact> foundContacts = new LinkedList<Contact>();
|
|
||||||
for (WebDavResource member : collection.getMembers()) {
|
|
||||||
Contact c = new Contact(member.getName(), member.getETag());
|
|
||||||
c.parseEntity(member.getContent());
|
|
||||||
foundContacts.add(c);
|
|
||||||
}
|
|
||||||
return foundContacts.toArray(new Contact[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public CardDavAddressBook(String baseURL, String user, String password) throws IOException, URISyntaxException {
|
||||||
|
super(baseURL, user, password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,6 @@ import ezvcard.types.UrlType;
|
|||||||
public class Contact extends Resource {
|
public class Contact extends Resource {
|
||||||
public final String VCARD_STARRED = "X-DAVDROID-STARRED";
|
public final String VCARD_STARRED = "X-DAVDROID-STARRED";
|
||||||
|
|
||||||
@Getter @Setter private String uid;
|
|
||||||
|
|
||||||
// VCard data
|
|
||||||
@Getter @Setter boolean starred;
|
@Getter @Setter boolean starred;
|
||||||
|
|
||||||
@Getter @Setter private String displayName;
|
@Getter @Setter private String displayName;
|
||||||
|
@ -15,7 +15,6 @@ import java.util.List;
|
|||||||
import java.util.SimpleTimeZone;
|
import java.util.SimpleTimeZone;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import ezvcard.types.PhotoType;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.fortuna.ical4j.data.CalendarBuilder;
|
import net.fortuna.ical4j.data.CalendarBuilder;
|
||||||
@ -56,7 +55,7 @@ public class Event extends Resource {
|
|||||||
|
|
||||||
private TimeZoneRegistry tzRegistry;
|
private TimeZoneRegistry tzRegistry;
|
||||||
|
|
||||||
@Getter @Setter private String uid, summary, location, description;
|
@Getter @Setter private String summary, location, description;
|
||||||
|
|
||||||
@Getter private DtStart dtStart;
|
@Getter private DtStart dtStart;
|
||||||
@Getter private DtEnd dtEnd;
|
@Getter private DtEnd dtEnd;
|
||||||
|
@ -7,10 +7,7 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
@ -19,12 +16,8 @@ import android.content.ContentProviderOperation;
|
|||||||
import android.content.ContentProviderOperation.Builder;
|
import android.content.ContentProviderOperation.Builder;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.CalendarContract;
|
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.provider.CalendarContract.Events;
|
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Email;
|
import android.provider.ContactsContract.CommonDataKinds.Email;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Nickname;
|
import android.provider.ContactsContract.CommonDataKinds.Nickname;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Note;
|
import android.provider.ContactsContract.CommonDataKinds.Note;
|
||||||
@ -35,9 +28,6 @@ import android.provider.ContactsContract.CommonDataKinds.Website;
|
|||||||
import android.provider.ContactsContract.Data;
|
import android.provider.ContactsContract.Data;
|
||||||
import android.provider.ContactsContract.RawContacts;
|
import android.provider.ContactsContract.RawContacts;
|
||||||
import at.bitfire.davdroid.Constants;
|
import at.bitfire.davdroid.Constants;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
|
|
||||||
import ezvcard.parameters.EmailTypeParameter;
|
import ezvcard.parameters.EmailTypeParameter;
|
||||||
import ezvcard.parameters.ImageTypeParameter;
|
import ezvcard.parameters.ImageTypeParameter;
|
||||||
import ezvcard.parameters.TelephoneTypeParameter;
|
import ezvcard.parameters.TelephoneTypeParameter;
|
||||||
@ -48,72 +38,40 @@ import ezvcard.types.PhotoType;
|
|||||||
import ezvcard.types.TelephoneType;
|
import ezvcard.types.TelephoneType;
|
||||||
import ezvcard.types.UrlType;
|
import ezvcard.types.UrlType;
|
||||||
|
|
||||||
public class LocalAddressBook extends LocalCollection {
|
public class LocalAddressBook extends LocalCollection<Contact> {
|
||||||
//private final static String TAG = "davdroid.LocalAddressBook";
|
//private final static String TAG = "davdroid.LocalAddressBook";
|
||||||
|
|
||||||
protected final static String
|
|
||||||
COLUMN_REMOTE_NAME = RawContacts.SOURCE_ID,
|
|
||||||
COLUMN_UID = RawContacts.SYNC1,
|
|
||||||
COLUMN_ETAG = RawContacts.SYNC2;
|
|
||||||
|
|
||||||
protected AccountManager accountManager;
|
protected AccountManager accountManager;
|
||||||
|
|
||||||
|
|
||||||
|
/* database fields */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Uri entriesURI() {
|
||||||
|
return syncAdapterURI(RawContacts.CONTENT_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String entryColumnAccountType() { return RawContacts.ACCOUNT_TYPE; }
|
||||||
|
protected String entryColumnAccountName() { return RawContacts.ACCOUNT_NAME; }
|
||||||
|
|
||||||
|
protected String entryColumnID() { return RawContacts._ID; }
|
||||||
|
protected String entryColumnRemoteName() { return RawContacts.SOURCE_ID; }
|
||||||
|
protected String entryColumnETag() { return RawContacts.SYNC2; }
|
||||||
|
|
||||||
|
protected String entryColumnDirty() { return RawContacts.DIRTY; }
|
||||||
|
protected String entryColumnDeleted() { return RawContacts.DELETED; }
|
||||||
|
|
||||||
|
protected String entryColumnUID() { return RawContacts.SYNC1; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public LocalAddressBook(Account account, ContentProviderClient providerClient, AccountManager accountManager) {
|
public LocalAddressBook(Account account, ContentProviderClient providerClient, AccountManager accountManager) {
|
||||||
super(account, providerClient);
|
super(account, providerClient);
|
||||||
this.accountManager = accountManager;
|
this.accountManager = accountManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* find multiple rows */
|
/* collection operations */
|
||||||
|
|
||||||
@Override
|
|
||||||
public Contact[] findDeleted() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
|
||||||
new String[] { RawContacts._ID, COLUMN_REMOTE_NAME, COLUMN_ETAG },
|
|
||||||
RawContacts.DELETED + "=1", null, null);
|
|
||||||
LinkedList<Contact> contacts = new LinkedList<Contact>();
|
|
||||||
while (cursor.moveToNext())
|
|
||||||
contacts.add(new Contact(cursor.getLong(0), cursor.getString(1), cursor.getString(2)));
|
|
||||||
return contacts.toArray(new Contact[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Contact[] findDirty() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
|
||||||
new String[] { RawContacts._ID, COLUMN_REMOTE_NAME, COLUMN_ETAG },
|
|
||||||
RawContacts.DIRTY + "=1", null, null);
|
|
||||||
LinkedList<Contact> contacts = new LinkedList<Contact>();
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
Contact c = new Contact(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
|
||||||
populate(c);
|
|
||||||
contacts.add(c);
|
|
||||||
}
|
|
||||||
return contacts.toArray(new Contact[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Contact[] findNew() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(), new String[] { RawContacts._ID },
|
|
||||||
RawContacts.DIRTY + "=1 AND " + COLUMN_REMOTE_NAME + " IS NULL", null, null);
|
|
||||||
LinkedList<Contact> contacts = new LinkedList<Contact>();
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String uid = UUID.randomUUID().toString(),
|
|
||||||
resourceName = uid + ".vcf";
|
|
||||||
Contact c = new Contact(cursor.getLong(0), resourceName, null);
|
|
||||||
c.setUid(uid);
|
|
||||||
populate(c);
|
|
||||||
|
|
||||||
// new record: set resource name and UID in database
|
|
||||||
pendingOperations.add(ContentProviderOperation
|
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), c.getLocalID()))
|
|
||||||
.withValue(COLUMN_REMOTE_NAME, resourceName)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
contacts.add(c);
|
|
||||||
}
|
|
||||||
return contacts.toArray(new Contact[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCTag() {
|
public String getCTag() {
|
||||||
@ -126,13 +84,21 @@ public class LocalAddressBook extends LocalCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* get data */
|
/* content provider (= database) querying */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Contact getByRemoteName(String remoteName) throws RemoteException {
|
public Contact findById(long localID, String remoteName, String eTag, boolean populate) throws RemoteException {
|
||||||
|
Contact c = new Contact(localID, remoteName, eTag);
|
||||||
|
if (populate)
|
||||||
|
populate(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Contact findByRemoteName(String remoteName) throws RemoteException {
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
Cursor cursor = providerClient.query(entriesURI(),
|
||||||
new String[] { RawContacts._ID, COLUMN_REMOTE_NAME, COLUMN_ETAG },
|
new String[] { RawContacts._ID, entryColumnRemoteName(), entryColumnETag() },
|
||||||
COLUMN_REMOTE_NAME + "=?", new String[] { remoteName }, null);
|
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
|
||||||
if (cursor.moveToNext())
|
if (cursor.moveToNext())
|
||||||
return new Contact(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
return new Contact(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||||
return null;
|
return null;
|
||||||
@ -145,7 +111,7 @@ public class LocalAddressBook extends LocalCollection {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
|
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
|
||||||
new String[] { COLUMN_UID, RawContacts.STARRED }, null, null, null);
|
new String[] { entryColumnUID(), RawContacts.STARRED }, null, null, null);
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
c.setUid(cursor.getString(0));
|
c.setUid(cursor.getString(0));
|
||||||
c.setStarred(cursor.getInt(1) != 0);
|
c.setStarred(cursor.getInt(1) != 0);
|
||||||
@ -262,131 +228,68 @@ public class LocalAddressBook extends LocalCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* create/update */
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void add(Resource resource) {
|
|
||||||
Contact contact = (Contact)resource;
|
|
||||||
|
|
||||||
int idx = pendingOperations.size();
|
|
||||||
pendingOperations.add(ContentProviderOperation.newInsert(entriesURI())
|
|
||||||
.withValue(RawContacts.ACCOUNT_NAME, account.name)
|
|
||||||
.withValue(RawContacts.ACCOUNT_TYPE, account.type)
|
|
||||||
.withValue(COLUMN_REMOTE_NAME, contact.getName())
|
|
||||||
.withValue(COLUMN_UID, contact.getUid())
|
|
||||||
.withValue(COLUMN_ETAG, contact.getETag())
|
|
||||||
.withValue(RawContacts.STARRED, contact.isStarred())
|
|
||||||
.withYieldAllowed(true)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
addDataRows(contact, -1, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateByRemoteName(Resource resource) throws RemoteException {
|
|
||||||
Contact remoteContact = (Contact)resource,
|
|
||||||
localContact = getByRemoteName(remoteContact.getName());
|
|
||||||
|
|
||||||
pendingOperations.add(ContentProviderOperation
|
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), localContact.getLocalID()))
|
|
||||||
.withValue(COLUMN_ETAG, remoteContact.getETag())
|
|
||||||
.withValue(RawContacts.STARRED, remoteContact.isStarred())
|
|
||||||
.withYieldAllowed(true)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
// remove all data rows ...
|
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(dataURI())
|
|
||||||
.withSelection(Data.RAW_CONTACT_ID + "=?",
|
|
||||||
new String[] { String.valueOf(localContact.getLocalID()) }).build());
|
|
||||||
|
|
||||||
// ... and insert new ones
|
|
||||||
addDataRows(remoteContact, localContact.getLocalID(), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
|
||||||
Builder builder = ContentProviderOperation.newDelete(entriesURI());
|
|
||||||
|
|
||||||
if (remoteResources.length != 0) {
|
|
||||||
List<String> terms = new LinkedList<String>();
|
|
||||||
for (Resource res : remoteResources)
|
|
||||||
terms.add(COLUMN_REMOTE_NAME + "<>" + DatabaseUtils.sqlEscapeString(res.getName()));
|
|
||||||
String where = Joiner.on(" AND ").join(terms);
|
|
||||||
builder = builder.withSelection(where, new String[] {});
|
|
||||||
} else
|
|
||||||
builder = builder.withSelection(COLUMN_REMOTE_NAME + " IS NOT NULL", null);
|
|
||||||
|
|
||||||
pendingOperations.add(builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* private helper methods */
|
/* private helper methods */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Uri syncAdapterURI(Uri baseURI) {
|
protected String fileExtension() {
|
||||||
return baseURI.buildUpon()
|
return ".vcf";
|
||||||
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
|
|
||||||
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
|
|
||||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Uri dataURI() {
|
protected Uri dataURI() {
|
||||||
return syncAdapterURI(Data.CONTENT_URI);
|
return syncAdapterURI(Data.CONTENT_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
|
||||||
protected Uri entriesURI() {
|
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
|
||||||
return RawContacts.CONTENT_URI.buildUpon()
|
|
||||||
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
|
|
||||||
.appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
|
|
||||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearDirty(Resource resource) {
|
|
||||||
pendingOperations.add(ContentProviderOperation
|
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
|
||||||
.withValue(RawContacts.DIRTY, 0).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Builder newInsertBuilder(long raw_contact_id, Integer backrefIdx) {
|
|
||||||
Builder builder = ContentProviderOperation.newInsert(dataURI());
|
|
||||||
if (backrefIdx != -1)
|
|
||||||
return builder.withValueBackReference(Data.RAW_CONTACT_ID, backrefIdx);
|
|
||||||
else
|
|
||||||
return builder.withValue(Data.RAW_CONTACT_ID, raw_contact_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void addDataRows(Contact contact, long localID, int backrefIdx) {
|
|
||||||
pendingOperations.add(buildStructuredName(newInsertBuilder(localID, backrefIdx), contact).build());
|
|
||||||
|
|
||||||
if (contact.getNickNames() != null)
|
|
||||||
for (String nick : contact.getNickNames().getValues())
|
|
||||||
pendingOperations.add(buildNickName(newInsertBuilder(localID, backrefIdx), nick).build());
|
|
||||||
|
|
||||||
for (PhotoType photo : contact.getPhotos())
|
|
||||||
pendingOperations.add(buildPhoto(newInsertBuilder(localID, backrefIdx), photo).build());
|
|
||||||
|
|
||||||
for (TelephoneType number : contact.getPhoneNumbers())
|
|
||||||
pendingOperations.add(buildPhoneNumber(newInsertBuilder(localID, backrefIdx), number).build());
|
|
||||||
|
|
||||||
for (EmailType email : contact.getEmails())
|
|
||||||
pendingOperations.add(buildEmail(newInsertBuilder(localID, backrefIdx), email).build());
|
|
||||||
|
|
||||||
for (UrlType url : contact.getURLs())
|
|
||||||
pendingOperations.add(buildURL(newInsertBuilder(localID, backrefIdx), url).build());
|
|
||||||
|
|
||||||
for (NoteType note : contact.getNotes())
|
|
||||||
pendingOperations.add(buildNote(newInsertBuilder(localID, backrefIdx), note).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* content builder methods */
|
/* content builder methods */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Builder buildEntry(Builder builder, Contact contact) {
|
||||||
|
return builder
|
||||||
|
.withValue(RawContacts.ACCOUNT_NAME, account.name)
|
||||||
|
.withValue(RawContacts.ACCOUNT_TYPE, account.type)
|
||||||
|
.withValue(entryColumnRemoteName(), contact.getName())
|
||||||
|
.withValue(entryColumnUID(), contact.getUid())
|
||||||
|
.withValue(entryColumnETag(), contact.getETag())
|
||||||
|
.withValue(RawContacts.STARRED, contact.isStarred());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addDataRows(Contact contact, long localID, int backrefIdx) {
|
||||||
|
pendingOperations.add(buildStructuredName(newDataInsertBuilder(localID, backrefIdx), contact).build());
|
||||||
|
|
||||||
|
if (contact.getNickNames() != null)
|
||||||
|
for (String nick : contact.getNickNames().getValues())
|
||||||
|
pendingOperations.add(buildNickName(newDataInsertBuilder(localID, backrefIdx), nick).build());
|
||||||
|
|
||||||
|
for (PhotoType photo : contact.getPhotos())
|
||||||
|
pendingOperations.add(buildPhoto(newDataInsertBuilder(localID, backrefIdx), photo).build());
|
||||||
|
|
||||||
|
for (TelephoneType number : contact.getPhoneNumbers())
|
||||||
|
pendingOperations.add(buildPhoneNumber(newDataInsertBuilder(localID, backrefIdx), number).build());
|
||||||
|
|
||||||
|
for (EmailType email : contact.getEmails())
|
||||||
|
pendingOperations.add(buildEmail(newDataInsertBuilder(localID, backrefIdx), email).build());
|
||||||
|
|
||||||
|
for (UrlType url : contact.getURLs())
|
||||||
|
pendingOperations.add(buildURL(newDataInsertBuilder(localID, backrefIdx), url).build());
|
||||||
|
|
||||||
|
for (NoteType note : contact.getNotes())
|
||||||
|
pendingOperations.add(buildNote(newDataInsertBuilder(localID, backrefIdx), note).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeDataRows(Contact contact) {
|
||||||
|
pendingOperations.add(ContentProviderOperation.newDelete(dataURI())
|
||||||
|
.withSelection(Data.RAW_CONTACT_ID + "=?",
|
||||||
|
new String[] { String.valueOf(contact.getLocalID()) }).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
protected Builder buildStructuredName(Builder builder, Contact contact) {
|
protected Builder buildStructuredName(Builder builder, Contact contact) {
|
||||||
return builder
|
return builder
|
||||||
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
|
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
|
||||||
|
@ -10,8 +10,6 @@ package at.bitfire.davdroid.resource;
|
|||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import net.fortuna.ical4j.model.Parameter;
|
import net.fortuna.ical4j.model.Parameter;
|
||||||
@ -36,33 +34,44 @@ import android.content.ContentResolver;
|
|||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
import android.provider.CalendarContract.Attendees;
|
import android.provider.CalendarContract.Attendees;
|
||||||
import android.provider.CalendarContract.Calendars;
|
import android.provider.CalendarContract.Calendars;
|
||||||
import android.provider.CalendarContract.Events;
|
import android.provider.CalendarContract.Events;
|
||||||
import android.provider.ContactsContract.RawContacts;
|
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import at.bitfire.davdroid.syncadapter.ServerInfo;
|
import at.bitfire.davdroid.syncadapter.ServerInfo;
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
public class LocalCalendar extends LocalCollection<Event> {
|
||||||
|
|
||||||
public class LocalCalendar extends LocalCollection {
|
|
||||||
private static final String TAG = "davdroid.LocalCalendar";
|
private static final String TAG = "davdroid.LocalCalendar";
|
||||||
|
|
||||||
protected final static String
|
|
||||||
CALENDARS_COLUMN_CTAG = Calendars.CAL_SYNC1,
|
|
||||||
EVENTS_COLUMN_REMOTE_NAME = Events._SYNC_ID,
|
|
||||||
EVENTS_COLUMN_ETAG = Events.SYNC_DATA1;
|
|
||||||
|
|
||||||
protected long id;
|
protected long id;
|
||||||
@Getter protected String path, cTag;
|
@Getter protected String path, cTag;
|
||||||
|
|
||||||
|
protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;
|
||||||
|
|
||||||
/* class methods */
|
|
||||||
|
/* database fields */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Uri entriesURI() {
|
||||||
|
return syncAdapterURI(Events.CONTENT_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; }
|
||||||
|
protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; }
|
||||||
|
|
||||||
|
protected String entryColumnID() { return Events._ID; }
|
||||||
|
protected String entryColumnRemoteName() { return Events._SYNC_ID; }
|
||||||
|
protected String entryColumnETag() { return Events.SYNC_DATA1; }
|
||||||
|
|
||||||
|
protected String entryColumnDirty() { return Events.DIRTY; }
|
||||||
|
protected String entryColumnDeleted() { return Events.DELETED; }
|
||||||
|
|
||||||
|
|
||||||
|
/* class methods, constructor */
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
public static void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws RemoteException {
|
public static void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws RemoteException {
|
||||||
@ -86,7 +95,7 @@ public class LocalCalendar extends LocalCollection {
|
|||||||
|
|
||||||
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
||||||
Cursor cursor = providerClient.query(calendarsURI(account),
|
Cursor cursor = providerClient.query(calendarsURI(account),
|
||||||
new String[] { Calendars._ID, Calendars.NAME, CALENDARS_COLUMN_CTAG },
|
new String[] { Calendars._ID, Calendars.NAME, COLLECTION_COLUMN_CTAG },
|
||||||
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
|
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
|
||||||
LinkedList<LocalCalendar> calendars = new LinkedList<LocalCalendar>();
|
LinkedList<LocalCalendar> calendars = new LinkedList<LocalCalendar>();
|
||||||
while (cursor.moveToNext())
|
while (cursor.moveToNext())
|
||||||
@ -102,70 +111,32 @@ public class LocalCalendar extends LocalCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* find multiple rows */
|
/* collection operations */
|
||||||
|
|
||||||
@Override
|
|
||||||
public Resource[] findDeleted() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
|
||||||
new String[] { Events._ID, EVENTS_COLUMN_REMOTE_NAME, EVENTS_COLUMN_ETAG },
|
|
||||||
Events.CALENDAR_ID + "=? AND " + Events.DELETED + "=1", new String[] { String.valueOf(id) }, null);
|
|
||||||
LinkedList<Event> events = new LinkedList<Event>();
|
|
||||||
while (cursor.moveToNext())
|
|
||||||
events.add(new Event(cursor.getLong(0), cursor.getString(1), cursor.getString(2)));
|
|
||||||
return events.toArray(new Event[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Resource[] findDirty() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
|
||||||
new String[] { Events._ID, EVENTS_COLUMN_REMOTE_NAME, EVENTS_COLUMN_ETAG },
|
|
||||||
Events.DIRTY + "=1", null, null);
|
|
||||||
LinkedList<Event> events = new LinkedList<Event>();
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
Event e = new Event(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
|
||||||
populate(e);
|
|
||||||
events.add(e);
|
|
||||||
}
|
|
||||||
return events.toArray(new Event[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Resource[] findNew() throws RemoteException {
|
|
||||||
Cursor cursor = providerClient.query(entriesURI(), new String[] { Events._ID },
|
|
||||||
Events.DIRTY + "=1 AND " + EVENTS_COLUMN_REMOTE_NAME + " IS NULL", null, null);
|
|
||||||
LinkedList<Event> events = new LinkedList<Event>();
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String uid = UUID.randomUUID().toString(),
|
|
||||||
resourceName = uid + ".ics";
|
|
||||||
Event e = new Event(cursor.getLong(0), resourceName, null);
|
|
||||||
e.setUid(uid);
|
|
||||||
populate(e);
|
|
||||||
|
|
||||||
// new record: set resource name and UID in database
|
|
||||||
pendingOperations.add(ContentProviderOperation
|
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), e.getLocalID()))
|
|
||||||
.withValue(EVENTS_COLUMN_REMOTE_NAME, resourceName)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
events.add(e);
|
|
||||||
}
|
|
||||||
return events.toArray(new Event[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCTag(String cTag) {
|
public void setCTag(String cTag) {
|
||||||
pendingOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(calendarsURI(), id))
|
pendingOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(calendarsURI(), id))
|
||||||
.withValue(CALENDARS_COLUMN_CTAG, cTag)
|
.withValue(COLLECTION_COLUMN_CTAG, cTag)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* get data */
|
/* content provider (= database) querying */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Event getByRemoteName(String remoteName) throws RemoteException {
|
public Event findById(long localID, String remoteName, String eTag, boolean populate) throws RemoteException {
|
||||||
Cursor cursor = providerClient.query(entriesURI(), new String[] { Events._ID, EVENTS_COLUMN_REMOTE_NAME, EVENTS_COLUMN_ETAG },
|
Event e = new Event(localID, remoteName, eTag);
|
||||||
Events.CALENDAR_ID + "=? AND " + EVENTS_COLUMN_REMOTE_NAME + "=?", new String[] { String.valueOf(id), remoteName }, null);
|
if (populate)
|
||||||
|
populate(e);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Event findByRemoteName(String remoteName) throws RemoteException {
|
||||||
|
Cursor cursor = providerClient.query(entriesURI(),
|
||||||
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
|
Events.CALENDAR_ID + "=? AND " + entryColumnRemoteName() + "=?",
|
||||||
|
new String[] { String.valueOf(id), remoteName }, null);
|
||||||
if (cursor.moveToNext())
|
if (cursor.moveToNext())
|
||||||
return new Event(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
return new Event(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||||
return null;
|
return null;
|
||||||
@ -336,61 +307,13 @@ public class LocalCalendar extends LocalCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* create/update */
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void add(Resource resource) {
|
|
||||||
Event event = (Event) resource;
|
|
||||||
|
|
||||||
int idx = pendingOperations.size();
|
|
||||||
pendingOperations.add(buildEvent(ContentProviderOperation.newInsert(entriesURI()), event)
|
|
||||||
.withYieldAllowed(true)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
addDataRows(event, -1, idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateByRemoteName(Resource remoteResource) throws RemoteException {
|
|
||||||
Event remoteEvent = (Event) remoteResource,
|
|
||||||
localEvent = (Event) getByRemoteName(remoteResource.getName());
|
|
||||||
|
|
||||||
pendingOperations.add(buildEvent(
|
|
||||||
ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localEvent.getLocalID())), remoteEvent)
|
|
||||||
.withYieldAllowed(true).build());
|
|
||||||
|
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
|
||||||
.withSelection(Attendees.EVENT_ID + "=?",
|
|
||||||
new String[] { String.valueOf(localEvent.getLocalID()) }).build());
|
|
||||||
|
|
||||||
addDataRows(remoteEvent, localEvent.getLocalID(), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(Resource event) {
|
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(
|
|
||||||
ContentUris.withAppendedId(entriesURI(), event.getLocalID())).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
|
||||||
Builder builder = ContentProviderOperation.newDelete(entriesURI());
|
|
||||||
|
|
||||||
if (remoteResources.length != 0) {
|
|
||||||
List<String> terms = new LinkedList<String>();
|
|
||||||
for (Resource res : remoteResources)
|
|
||||||
terms.add(EVENTS_COLUMN_REMOTE_NAME + "<>" + DatabaseUtils.sqlEscapeString(res.getName()));
|
|
||||||
String where = Joiner.on(" AND ").join(terms);
|
|
||||||
builder = builder.withSelection(where, null);
|
|
||||||
} else
|
|
||||||
builder = builder.withSelection(EVENTS_COLUMN_REMOTE_NAME + " IS NOT NULL", null);
|
|
||||||
|
|
||||||
pendingOperations.add(builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* private helper methods */
|
/* private helper methods */
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String fileExtension() {
|
||||||
|
return ".ics";
|
||||||
|
}
|
||||||
|
|
||||||
protected static Uri calendarsURI(Account account) {
|
protected static Uri calendarsURI(Account account) {
|
||||||
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||||
@ -401,47 +324,16 @@ public class LocalCalendar extends LocalCollection {
|
|||||||
return calendarsURI(account);
|
return calendarsURI(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Uri syncAdapterURI(Uri baseURI) {
|
|
||||||
return baseURI.buildUpon()
|
|
||||||
.appendQueryParameter(Events.ACCOUNT_NAME, account.name)
|
|
||||||
.appendQueryParameter(Events.ACCOUNT_TYPE, account.type)
|
|
||||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Uri entriesURI() {
|
|
||||||
return syncAdapterURI(Events.CONTENT_URI);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearDirty(Resource resource) {
|
|
||||||
pendingOperations.add(ContentProviderOperation
|
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
|
||||||
.withValue(Events.DIRTY, 0).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Builder newInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
|
|
||||||
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
|
||||||
if (backrefIdx != -1)
|
|
||||||
return builder.withValueBackReference(refFieldName, backrefIdx);
|
|
||||||
else
|
|
||||||
return builder.withValue(refFieldName, raw_ref_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void addDataRows(Event event, long localID, int backrefIdx) {
|
|
||||||
for (Attendee attendee : event.getAttendees())
|
|
||||||
pendingOperations.add(buildAttendee(newInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* content builder methods */
|
/* content builder methods */
|
||||||
|
|
||||||
protected Builder buildEvent(Builder builder, Event event) {
|
@Override
|
||||||
builder = builder.withValue(Events.CALENDAR_ID, id)
|
protected Builder buildEntry(Builder builder, Event event) {
|
||||||
.withValue(EVENTS_COLUMN_REMOTE_NAME, event.getName())
|
builder = builder
|
||||||
.withValue(EVENTS_COLUMN_ETAG, event.getETag())
|
.withValue(Events.CALENDAR_ID, id)
|
||||||
|
.withValue(entryColumnRemoteName(), event.getName())
|
||||||
|
.withValue(entryColumnETag(), event.getETag())
|
||||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||||
.withValue(Events.DTEND, event.getDtEndInMillis())
|
.withValue(Events.DTEND, event.getDtEndInMillis())
|
||||||
@ -483,6 +375,21 @@ public class LocalCalendar extends LocalCollection {
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addDataRows(Event event, long localID, int backrefIdx) {
|
||||||
|
for (Attendee attendee : event.getAttendees())
|
||||||
|
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeDataRows(Event event) {
|
||||||
|
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
||||||
|
.withSelection(Attendees.EVENT_ID + "=?",
|
||||||
|
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
||||||
Uri member = Uri.parse(attendee.getValue());
|
Uri member = Uri.parse(attendee.getValue());
|
||||||
|
@ -8,61 +8,195 @@
|
|||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentProviderOperation;
|
import android.content.ContentProviderOperation;
|
||||||
|
import android.content.ContentProviderOperation.Builder;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.OperationApplicationException;
|
import android.content.OperationApplicationException;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DatabaseUtils;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.CalendarContract;
|
||||||
import android.provider.CalendarContract.Events;
|
|
||||||
import android.provider.ContactsContract.RawContacts;
|
|
||||||
|
|
||||||
public abstract class LocalCollection {
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
|
public abstract class LocalCollection<ResourceType extends Resource> {
|
||||||
protected Account account;
|
protected Account account;
|
||||||
protected ContentProviderClient providerClient;
|
protected ContentProviderClient providerClient;
|
||||||
protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<ContentProviderOperation>();;
|
protected ArrayList<ContentProviderOperation> pendingOperations = new ArrayList<ContentProviderOperation>();
|
||||||
|
|
||||||
|
|
||||||
|
// database fields
|
||||||
|
|
||||||
|
abstract protected Uri entriesURI();
|
||||||
|
|
||||||
|
abstract protected String entryColumnAccountType();
|
||||||
|
abstract protected String entryColumnAccountName();
|
||||||
|
|
||||||
|
abstract protected String entryColumnID();
|
||||||
|
abstract protected String entryColumnRemoteName();
|
||||||
|
abstract protected String entryColumnETag();
|
||||||
|
|
||||||
|
abstract protected String entryColumnDirty();
|
||||||
|
abstract protected String entryColumnDeleted();
|
||||||
|
|
||||||
|
|
||||||
LocalCollection(Account account, ContentProviderClient providerClient) {
|
LocalCollection(Account account, ContentProviderClient providerClient) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
this.providerClient = providerClient;
|
this.providerClient = providerClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
// query
|
|
||||||
abstract public Resource[] findDeleted() throws RemoteException;
|
|
||||||
abstract public Resource[] findDirty() throws RemoteException;
|
|
||||||
abstract public Resource[] findNew() throws RemoteException;
|
|
||||||
|
|
||||||
// cache management
|
// collection operations
|
||||||
|
|
||||||
abstract public String getCTag();
|
abstract public String getCTag();
|
||||||
abstract public void setCTag(String cTag);
|
abstract public void setCTag(String cTag);
|
||||||
|
|
||||||
// fetch
|
|
||||||
public abstract Resource getByRemoteName(String name) throws RemoteException;
|
|
||||||
public abstract void populate(Resource record) throws RemoteException;
|
|
||||||
|
|
||||||
// modify
|
// content provider (= database) querying
|
||||||
public abstract void add(Resource resource);
|
|
||||||
public abstract void updateByRemoteName(Resource remoteResource) throws RemoteException;
|
|
||||||
|
|
||||||
public void delete(Resource resource) {
|
public Resource[] findDirty() throws RemoteException {
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(
|
Cursor cursor = providerClient.query(entriesURI(),
|
||||||
ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
.build());
|
entryColumnDirty() + "=1", null, null);
|
||||||
|
LinkedList<Resource> dirty = new LinkedList<Resource>();
|
||||||
|
while (cursor.moveToNext())
|
||||||
|
dirty.add(findById(cursor.getLong(0), cursor.getString(1), cursor.getString(2), true));
|
||||||
|
return dirty.toArray(new Resource[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void deleteAllExceptRemoteNames(Resource[] remoteRecords);
|
public Resource[] findDeleted() throws RemoteException {
|
||||||
|
Cursor cursor = providerClient.query(entriesURI(),
|
||||||
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
|
entryColumnDeleted() + "=1", null, null);
|
||||||
|
LinkedList<Resource> deleted = new LinkedList<Resource>();
|
||||||
|
while (cursor.moveToNext())
|
||||||
|
deleted.add(findById(cursor.getLong(0), cursor.getString(1), cursor.getString(2), false));
|
||||||
|
return deleted.toArray(new Resource[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// database operations
|
public Resource[] findNew() throws RemoteException {
|
||||||
protected abstract Uri syncAdapterURI(Uri baseURI);
|
Cursor cursor = providerClient.query(entriesURI(),
|
||||||
protected abstract Uri entriesURI();
|
new String[] { entryColumnID() },
|
||||||
public abstract void clearDirty(Resource resource);
|
entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NULL", null, null);
|
||||||
|
LinkedList<Resource> fresh = new LinkedList<Resource>();
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
String uid = UUID.randomUUID().toString(),
|
||||||
|
resourceName = uid + fileExtension();
|
||||||
|
Resource resource = findById(cursor.getLong(0), resourceName, null, true); //new Event(cursor.getLong(0), resourceName, null);
|
||||||
|
resource.setUid(uid);
|
||||||
|
|
||||||
|
// new record: set generated resource name in database
|
||||||
|
pendingOperations.add(ContentProviderOperation
|
||||||
|
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||||
|
.withValue(entryColumnRemoteName(), resourceName)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
fresh.add(resource);
|
||||||
|
}
|
||||||
|
return fresh.toArray(new Event[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public Resource findById(long localID, String resourceName, String eTag, boolean populate) throws RemoteException;
|
||||||
|
abstract public ResourceType findByRemoteName(String name) throws RemoteException;
|
||||||
|
|
||||||
|
public abstract void populate(Resource record) throws RemoteException;
|
||||||
|
|
||||||
|
|
||||||
|
// create/update/delete
|
||||||
|
|
||||||
|
public void add(ResourceType resource) {
|
||||||
|
int idx = pendingOperations.size();
|
||||||
|
pendingOperations.add(
|
||||||
|
buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource)
|
||||||
|
.withYieldAllowed(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
addDataRows(resource, -1, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateByRemoteName(ResourceType remoteResource) throws RemoteException {
|
||||||
|
ResourceType localResource = findByRemoteName(remoteResource.getName());
|
||||||
|
|
||||||
|
pendingOperations.add(
|
||||||
|
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
|
||||||
|
.withValue(entryColumnETag(), remoteResource.getETag())
|
||||||
|
.withYieldAllowed(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
removeDataRows(localResource);
|
||||||
|
addDataRows(remoteResource, localResource.getLocalID(), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Resource resource) {
|
||||||
|
pendingOperations.add(ContentProviderOperation
|
||||||
|
.newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||||
|
.withYieldAllowed(true)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
||||||
|
Builder builder = ContentProviderOperation.newDelete(entriesURI());
|
||||||
|
|
||||||
|
if (remoteResources.length != 0) {
|
||||||
|
List<String> terms = new LinkedList<String>();
|
||||||
|
for (Resource res : remoteResources)
|
||||||
|
terms.add(entryColumnRemoteName() + "<>" + DatabaseUtils.sqlEscapeString(res.getName()));
|
||||||
|
String where = Joiner.on(" AND ").join(terms);
|
||||||
|
builder = builder.withSelection(where, new String[] {});
|
||||||
|
} else
|
||||||
|
builder = builder.withSelection(entryColumnRemoteName() + " IS NOT NULL", null);
|
||||||
|
|
||||||
|
pendingOperations.add(builder
|
||||||
|
.withYieldAllowed(true)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearDirty(Resource resource) {
|
||||||
|
pendingOperations.add(ContentProviderOperation
|
||||||
|
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||||
|
.withValue(entryColumnDirty(), 0).build());
|
||||||
|
}
|
||||||
|
|
||||||
public void commit() throws RemoteException, OperationApplicationException {
|
public void commit() throws RemoteException, OperationApplicationException {
|
||||||
if (!pendingOperations.isEmpty())
|
if (!pendingOperations.isEmpty())
|
||||||
providerClient.applyBatch(pendingOperations);
|
providerClient.applyBatch(pendingOperations);
|
||||||
|
|
||||||
pendingOperations.clear();
|
pendingOperations.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
protected abstract String fileExtension();
|
||||||
|
|
||||||
|
protected Uri syncAdapterURI(Uri baseURI) {
|
||||||
|
return baseURI.buildUpon()
|
||||||
|
.appendQueryParameter(entryColumnAccountType(), account.type)
|
||||||
|
.appendQueryParameter(entryColumnAccountName(), account.name)
|
||||||
|
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
|
||||||
|
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
||||||
|
if (backrefIdx != -1)
|
||||||
|
return builder.withValueBackReference(refFieldName, backrefIdx);
|
||||||
|
else
|
||||||
|
return builder.withValue(refFieldName, raw_ref_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// content builders
|
||||||
|
|
||||||
|
protected abstract Builder buildEntry(Builder builder, ResourceType resource);
|
||||||
|
|
||||||
|
protected abstract void addDataRows(ResourceType resource, long localID, int backrefIdx);
|
||||||
|
protected abstract void removeDataRows(ResourceType resource);
|
||||||
}
|
}
|
||||||
|
@ -8,24 +8,35 @@
|
|||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import net.fortuna.ical4j.data.ParserException;
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
|
|
||||||
import org.apache.http.HttpException;
|
import org.apache.http.HttpException;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import at.bitfire.davdroid.webdav.HttpPropfind;
|
import at.bitfire.davdroid.webdav.HttpPropfind;
|
||||||
import at.bitfire.davdroid.webdav.WebDavCollection;
|
import at.bitfire.davdroid.webdav.WebDavCollection;
|
||||||
|
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
|
||||||
import at.bitfire.davdroid.webdav.WebDavResource;
|
import at.bitfire.davdroid.webdav.WebDavResource;
|
||||||
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
|
||||||
|
|
||||||
public abstract class RemoteCollection {
|
public abstract class RemoteCollection<ResourceType extends Resource> {
|
||||||
@Getter WebDavCollection collection;
|
@Getter WebDavCollection collection;
|
||||||
|
|
||||||
protected abstract String memberContentType();
|
abstract protected String memberContentType();
|
||||||
|
abstract protected MultigetType multiGetType();
|
||||||
|
abstract protected ResourceType newResourceSkeleton(String name, String ETag);
|
||||||
|
|
||||||
|
public RemoteCollection(String baseURL, String user, String password) throws IOException, URISyntaxException {
|
||||||
|
collection = new WebDavCollection(new URI(baseURL), user, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* collection methods */
|
/* collection operations */
|
||||||
|
|
||||||
public String getCTag() throws IOException, HttpException {
|
public String getCTag() throws IOException, HttpException {
|
||||||
try {
|
try {
|
||||||
@ -39,33 +50,57 @@ public abstract class RemoteCollection {
|
|||||||
|
|
||||||
public Resource[] getMemberETags() throws IOException, IncapableResourceException, HttpException {
|
public Resource[] getMemberETags() throws IOException, IncapableResourceException, HttpException {
|
||||||
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
|
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
|
||||||
return null;
|
|
||||||
|
List<ResourceType> resources = new LinkedList<ResourceType>();
|
||||||
|
for (WebDavResource member : collection.getMembers())
|
||||||
|
resources.add(newResourceSkeleton(member.getName(), member.getETag()));
|
||||||
|
return resources.toArray(new Resource[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Resource[] multiGet(Resource[] resource) throws IOException, IncapableResourceException, HttpException, ParserException;
|
@SuppressWarnings("unchecked")
|
||||||
|
public Resource[] multiGet(ResourceType[] resources) throws IOException, IncapableResourceException, HttpException, ParserException {
|
||||||
|
if (resources.length == 1) {
|
||||||
|
Resource resource = get(resources[0]);
|
||||||
|
return (resource != null) ? (ResourceType[]) new Resource[] { resource } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedList<String> names = new LinkedList<String>();
|
||||||
|
for (ResourceType resource : resources)
|
||||||
|
names.add(resource.getName());
|
||||||
|
|
||||||
|
collection.multiGet(names.toArray(new String[0]), multiGetType());
|
||||||
|
|
||||||
|
LinkedList<ResourceType> foundResources = new LinkedList<ResourceType>();
|
||||||
|
for (WebDavResource member : collection.getMembers()) {
|
||||||
|
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
|
||||||
|
resource.parseEntity(member.getContent());
|
||||||
|
foundResources.add(resource);
|
||||||
|
}
|
||||||
|
return foundResources.toArray(new Resource[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* internal member methods */
|
/* internal member operations */
|
||||||
|
|
||||||
public Resource get(Resource resource) throws IOException, HttpException, ParserException {
|
public ResourceType get(ResourceType resource) throws IOException, HttpException, ParserException {
|
||||||
WebDavResource member = new WebDavResource(collection, resource.getName());
|
WebDavResource member = new WebDavResource(collection, resource.getName());
|
||||||
member.get();
|
member.get();
|
||||||
resource.parseEntity(member.getContent());
|
resource.parseEntity(member.getContent());
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(Resource resource) throws IOException, HttpException {
|
public void add(ResourceType resource) throws IOException, HttpException {
|
||||||
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
||||||
member.setContentType(memberContentType());
|
member.setContentType(memberContentType());
|
||||||
member.put(resource.toEntity().getBytes("UTF-8"), PutMode.ADD_DONT_OVERWRITE);
|
member.put(resource.toEntity().getBytes("UTF-8"), PutMode.ADD_DONT_OVERWRITE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Resource resource) throws IOException, HttpException {
|
public void delete(ResourceType resource) throws IOException, HttpException {
|
||||||
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
||||||
member.delete();
|
member.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(Resource resource) throws IOException, HttpException {
|
public void update(ResourceType resource) throws IOException, HttpException {
|
||||||
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
WebDavResource member = new WebDavResource(collection, resource.getName(), resource.getETag());
|
||||||
member.setContentType(memberContentType());
|
member.setContentType(memberContentType());
|
||||||
member.put(resource.toEntity().getBytes("UTF-8"), PutMode.UPDATE_DONT_OVERWRITE);
|
member.put(resource.toEntity().getBytes("UTF-8"), PutMode.UPDATE_DONT_OVERWRITE);
|
||||||
|
@ -12,11 +12,13 @@ import java.io.InputStream;
|
|||||||
|
|
||||||
import net.fortuna.ical4j.data.ParserException;
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
@ToString
|
@ToString
|
||||||
public abstract class Resource {
|
public abstract class Resource {
|
||||||
@Getter protected String name, ETag;
|
@Getter protected String name, ETag;
|
||||||
|
@Getter @Setter protected String uid;
|
||||||
@Getter protected long localID;
|
@Getter protected long localID;
|
||||||
|
|
||||||
@Getter protected boolean populated = false;
|
@Getter protected boolean populated = false;
|
||||||
|
@ -120,7 +120,7 @@ public class SyncManager {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
for (Resource remoteResource : remoteResources) {
|
for (Resource remoteResource : remoteResources) {
|
||||||
Resource localResource = local.getByRemoteName(remoteResource.getName());
|
Resource localResource = local.findByRemoteName(remoteResource.getName());
|
||||||
if (localResource == null)
|
if (localResource == null)
|
||||||
resourcesToAdd.add(remoteResource);
|
resourcesToAdd.add(remoteResource);
|
||||||
else if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
|
else if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2013 Richard Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
package at.bitfire.davdroid.test;
|
package at.bitfire.davdroid.test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import junit.framework.Assert;
|
import junit.framework.Assert;
|
||||||
import net.fortuna.ical4j.data.ParserException;
|
import net.fortuna.ical4j.data.ParserException;
|
||||||
import net.fortuna.ical4j.model.Date;
|
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import android.text.format.Time;
|
|
||||||
import at.bitfire.davdroid.resource.Event;
|
import at.bitfire.davdroid.resource.Event;
|
||||||
|
|
||||||
public class CalendarTest extends InstrumentationTestCase {
|
public class CalendarTest extends InstrumentationTestCase {
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (c) 2013 Richard Hirner (bitfire web engineering).
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the GNU Public License v3.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.gnu.org/licenses/gpl.html
|
||||||
|
******************************************************************************/
|
||||||
package at.bitfire.davdroid.test;
|
package at.bitfire.davdroid.test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|