Commit a3181d35 authored by Ricki Hirner's avatar Ricki Hirner

Rewrite resource package to Kotlin

parent 49484710
......@@ -71,8 +71,9 @@ public class App extends Application {
at.bitfire.cert4android.Constants.log = Logger.getLogger("davdroid.cert4android");
}
@Getter
private static String addressBookAccountType;
public static String getAddressBookAccountType() { return addressBookAccountType; }
@Getter
private static String addressBooksAuthority;
......
/*
* 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 java.io.FileNotFoundException;
import java.util.List;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
public interface LocalCollection<T extends LocalResource> {
List<T> getDeleted() throws CalendarStorageException, ContactsStorageException;
List<T> getWithoutFileName() throws CalendarStorageException, ContactsStorageException;
List<T> getDirty() throws CalendarStorageException, ContactsStorageException, FileNotFoundException;
List<T> getAll() throws CalendarStorageException, ContactsStorageException;
String getCTag() throws CalendarStorageException, ContactsStorageException;
void setCTag(String cTag) throws CalendarStorageException, ContactsStorageException;
}
/*
* 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 at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import java.io.FileNotFoundException
interface LocalCollection<out T: LocalResource> {
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getDeleted(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getWithoutFileName(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
fun getDirty(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getAll(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getCTag(): String?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun setCTag(cTag: String?)
}
This diff is collapsed.
/*
* 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.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Events;
import android.support.annotation.NonNull;
import net.fortuna.ical4j.model.property.ProdId;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.ical4android.AndroidCalendar;
import at.bitfire.ical4android.AndroidEvent;
import at.bitfire.ical4android.AndroidEventFactory;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.ical4android.Event;
import lombok.Cleanup;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@TargetApi(17)
@ToString(of={ "fileName","eTag" }, callSuper=true)
public class LocalEvent extends AndroidEvent implements LocalResource {
static {
Event.setProdId(new ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x"));
}
static final String COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1,
COLUMN_UID = Build.VERSION.SDK_INT >= 17 ? Events.UID_2445 : Events.SYNC_DATA2,
COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3;
@Getter protected String fileName;
@Getter @Setter protected String eTag;
public boolean weAreOrganizer = true;
public LocalEvent(@NonNull AndroidCalendar calendar, Event event, String fileName, String eTag) {
super(calendar, event);
this.fileName = fileName;
this.eTag = eTag;
}
protected LocalEvent(@NonNull AndroidCalendar calendar, long id, ContentValues baseInfo) {
super(calendar, id, baseInfo);
if (baseInfo != null) {
fileName = baseInfo.getAsString(Events._SYNC_ID);
eTag = baseInfo.getAsString(COLUMN_ETAG);
}
}
/* process LocalEvent-specific fields */
@Override
protected void populateEvent(ContentValues values) throws FileNotFoundException, CalendarStorageException {
super.populateEvent(values);
fileName = values.getAsString(Events._SYNC_ID);
eTag = values.getAsString(COLUMN_ETAG);
getEvent().setUid(values.getAsString(COLUMN_UID));
getEvent().setSequence(values.getAsInteger(COLUMN_SEQUENCE));
if (Build.VERSION.SDK_INT >= 17) {
Integer isOrganizer = values.getAsInteger(Events.IS_ORGANIZER);
weAreOrganizer = isOrganizer != null && isOrganizer != 0;
} else {
String organizer = values.getAsString(Events.ORGANIZER);
weAreOrganizer = organizer == null || organizer.equals(getCalendar().getAccount().name);
}
}
@Override
protected void buildEvent(Event recurrence, ContentProviderOperation.Builder builder) throws FileNotFoundException, CalendarStorageException {
super.buildEvent(recurrence, builder);
boolean buildException = recurrence != null;
Event eventToBuild = buildException ? recurrence : getEvent();
builder .withValue(COLUMN_UID, getEvent().getUid())
.withValue(COLUMN_SEQUENCE, eventToBuild.getSequence())
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0);
if (buildException)
builder.withValue(Events.ORIGINAL_SYNC_ID, fileName);
else
builder .withValue(Events._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag);
}
/* custom queries */
public void prepareForUpload() throws CalendarStorageException {
try {
String uid = null;
@Cleanup Cursor c = getCalendar().getProvider().query(eventSyncURI(), new String[] { COLUMN_UID }, null, null, null);
if (c.moveToNext())
uid = c.getString(0);
if (uid == null)
uid = UUID.randomUUID().toString();
final String newFileName = uid + ".ics";
ContentValues values = new ContentValues(2);
values.put(Events._SYNC_ID, newFileName);
values.put(COLUMN_UID, uid);
getCalendar().getProvider().update(eventSyncURI(), values, null, null);
fileName = newFileName;
if (getEvent() != null)
getEvent().setUid(uid);
} catch (FileNotFoundException|RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
@Override
public void clearDirty(String eTag) throws CalendarStorageException {
try {
ContentValues values = new ContentValues(2);
values.put(CalendarContract.Events.DIRTY, 0);
values.put(COLUMN_ETAG, eTag);
if (getEvent() != null)
values.put(COLUMN_SEQUENCE, getEvent().getSequence());
getCalendar().getProvider().update(eventSyncURI(), values, null, null);
this.eTag = eTag;
} catch (IOException|RemoteException e) {
throw new CalendarStorageException("Couldn't update UID", e);
}
}
static class Factory implements AndroidEventFactory<LocalEvent> {
static final Factory INSTANCE = new Factory();
@Override
public LocalEvent newInstance(AndroidCalendar calendar, long id, ContentValues baseInfo) {
return new LocalEvent(calendar, id, baseInfo);
}
@Override
public LocalEvent newInstance(AndroidCalendar calendar, Event event) {
return new LocalEvent(calendar, event, null, null);
}
}
}
/*
* 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.ContentProviderOperation
import android.content.ContentValues
import android.os.Build
import android.provider.CalendarContract
import android.provider.CalendarContract.Events
import at.bitfire.davdroid.BuildConfig
import at.bitfire.ical4android.*
import net.fortuna.ical4j.model.property.ProdId
import java.io.FileNotFoundException
import java.util.*
//@TargetApi(17)
//TODO @ToString(of={ "fileName","eTag" }, callSuper=true)
class LocalEvent: AndroidEvent, LocalResource {
companion object {
init {
iCalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
}
val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2
val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
}
override var fileName: String? = null
override var eTag: String? = null
var weAreOrganizer = true
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?): super(calendar, event) {
this.fileName = fileName
this.eTag = eTag
}
private constructor(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?): super(calendar, id, baseInfo) {
baseInfo?.let {
fileName = it.getAsString(Events._SYNC_ID)
eTag = it.getAsString(COLUMN_ETAG)
}
}
/* process LocalEvent-specific fields */
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun populateEvent(row: ContentValues) {
super.populateEvent(row)
val event = requireNotNull(event)
fileName = row.getAsString(Events._SYNC_ID)
eTag = row.getAsString(COLUMN_ETAG)
event.uid = row.getAsString(COLUMN_UID)
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
if (Build.VERSION.SDK_INT >= 17) {
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
} else {
val organizer = row.getAsString(Events.ORGANIZER)
weAreOrganizer = organizer == null || organizer == calendar.account.name
}
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
super.buildEvent(recurrence, builder)
val event = requireNotNull(event)
val buildException = recurrence != null
val eventToBuild = recurrence ?: event
builder .withValue(COLUMN_UID, event.uid)
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0)
if (buildException)
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
else
builder .withValue(Events._SYNC_ID, fileName)
.withValue(COLUMN_ETAG, eTag)
}
/* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() {
try {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(COLUMN_UID, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName
event!!.uid = uid
} catch(e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SEQUENCE, event!!.sequence);
calendar.provider.update(eventSyncURI(), values, null, null)
this.eTag = eTag
} catch (e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
}
object Factory: AndroidEventFactory<LocalEvent> {
override fun newInstance(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?) =
LocalEvent(calendar, id, baseInfo)
override fun newInstance(calendar: AndroidCalendar<*>, event: Event) =
LocalEvent(calendar, event, null, null)
}
}
/*
* 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.ContentProviderOperation
import android.content.ContentUris
import android.content.ContentValues
import android.os.Build
import android.os.Parcel
import android.os.RemoteException
import android.provider.ContactsContract
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import android.provider.ContactsContract.RawContacts.Data
import at.bitfire.dav4android.Constants
import at.bitfire.vcard4android.*
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
// TODO @ToString(callSuper=true)
class LocalGroup: AndroidGroup, LocalResource {
companion object {
/** marshaled list of member UIDs, as sent by server */
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
/**
* Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships
* are (if possible) applied, keeping cached memberships in sync.
* @param addressBook address book to take groups from
* @throws ContactsStorageException on contact provider errors
*/
@JvmStatic
@Throws(ContactsStorageException::class)
fun applyPendingMemberships(addressBook: LocalAddressBook) {
try {
addressBook.provider!!.query(
addressBook.syncAdapterURI(Groups.CONTENT_URI),
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
null
)?.use { cursor ->
val batch = BatchOperation(addressBook.provider)
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
Constants.log.fine("Assigning members to group $id")
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
val changeContactIDs = HashSet<Long>()
// delete all memberships and cached memberships for this group
for (contact in addressBook.getByGroupMembership(id)) {
contact.removeGroupMemberships(batch)
changeContactIDs.add(contact.id!!)
}
// extract list of member UIDs
val members = LinkedList<String>()
val raw = cursor.getBlob(1)
val parcel = Parcel.obtain()
try {
parcel.unmarshall(raw, 0, raw.size)
parcel.setDataPosition(0)
parcel.readStringList(members)
} finally {
parcel.recycle()
}
// insert memberships
for (uid in members) {
Constants.log.fine("Assigning member: $uid")
try {
val member = addressBook.findContactByUID(uid)
member.addToGroup(batch, id)
changeContactIDs.add(member.id!!)
} catch(e: FileNotFoundException) {
Constants.log.log(Level.WARNING, "Group member not found: $uid", e)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
changeContactIDs
.map { LocalContact(addressBook, it, null, null) }
.forEach { it.updateHashCode(batch) }
// remove pending memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
))
batch.commit()
}
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't get pending memberships", e)
}
}
}
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?):
super(addressBook, id, fileName, eTag)
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?):
super(addressBook, contact, fileName, eTag)
@Throws(ContactsStorageException::class)
override fun clearDirty(eTag: String?) {
val id = requireNotNull(id)
val values = ContentValues(2)
values.put(Groups.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
this.eTag = eTag
update(values)
// update cached group memberships
val batch = BatchOperation(addressBook.provider!!)
// delete cached group memberships
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE, id.toString())
)
))
// insert updated cached group memberships
for (member in getMembers())
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
.withValue(CachedGroupMembership.RAW_CONTACT_ID, member)
.withValue(CachedGroupMembership.GROUP_ID, id)
.withYieldAllowed(true)
))
batch.commit()
}
@Throws(ContactsStorageException::class)
override fun prepareForUpload() {
val uid = UUID.randomUUID().toString()
val newFileName = "$uid.vcf"
val values = ContentValues(2)
values.put(COLUMN_FILENAME, newFileName)
values.put(COLUMN_UID, uid)
update(values)
fileName = newFileName
}
@Throws(FileNotFoundException::class, ContactsStorageException::class)
override fun contentValues(): ContentValues {
val values = super.contentValues()
val members = Parcel.obtain()
try {
members.writeStringList(contact!!.members)
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
} finally {
members.recycle()
}
return values
}
/**
* Marks all members of the current group as dirty.
*/
@Throws(ContactsStorageException::class)
fun markMembersDirty() {
val batch = BatchOperation(addressBook.provider!!)
for (member in getMembers())
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
.withValue(RawContacts.DIRTY, 1)
.withYieldAllowed(true)
))
batch.commit()
}
// helpers
/**
* Lists all members of this group.
* @return list of all members' raw contact IDs
* @throws ContactsStorageException on contact provider errorst
*/
@Throws(ContactsStorageException::class)
internal fun getMembers(): List<Long> {
val id = requireNotNull(id)
val members = LinkedList<Long>()
try {
addressBook.provider!!.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
arrayOf(Data.RAW_CONTACT_ID),
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
null
)?.use { cursor ->
while (cursor.moveToNext())
members.add(cursor.getLong(0))
}
} catch(e: RemoteException) {
throw ContactsStorageException("Couldn't list group members", e)
}
return members
}
// factory
object Factory: AndroidGroupFactory<LocalGroup> {
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, id: Long, fileName: String?, eTag: String?) =
LocalGroup(addressBook, id, fileName, eTag)
override fun newInstance(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?) =
LocalGroup(addressBook, contact, fileName, eTag)
}
}
......@@ -6,21 +6,25 @@
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.resource;
package at.bitfire.davdroid.resource
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.ContactsStorageException;
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
public interface LocalResource {
interface LocalResource {
Long getId();
val id: Long?
String getFileName();
String getETag();
var fileName: String?
var eTag: String?
int delete() throws CalendarStorageException, ContactsStorageException;
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun delete(): Int