You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/resource/LocalGroup.java

262 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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.database.Cursor;
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 org.apache.commons.lang3.ArrayUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import at.bitfire.davdroid.App;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidGroup;
import at.bitfire.vcard4android.AndroidGroupFactory;
import at.bitfire.vcard4android.BatchOperation;
import at.bitfire.vcard4android.CachedGroupMembership;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import lombok.Getter;
import lombok.ToString;
@ToString(callSuper=true)
public class LocalGroup extends AndroidGroup implements LocalResource {
@Getter
protected String uuid;
/** marshalled list of member UIDs, as sent by server */
public static final String COLUMN_PENDING_MEMBERS = Groups.SYNC3;
public LocalGroup(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
super(addressBook, id, fileName, eTag);
}
public LocalGroup(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
super(addressBook, contact, fileName, eTag);
}
@Override
public String getContent() throws IOException, ContactsStorageException {
return null;
}
@Override
public boolean isLocalOnly() {
return false;
}
@Override
public void clearDirty(String eTag) throws ContactsStorageException {
assertID();
ContentValues values = new ContentValues(2);
values.put(Groups.DIRTY, 0);
values.put(COLUMN_ETAG, this.eTag = eTag);
update(values);
// update cached group memberships
BatchOperation batch = new BatchOperation(addressBook.provider);
// delete cached group memberships
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?",
new String[] { CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) }
)
));
// insert updated cached group memberships
for (long member : getMembers())
batch.enqueue(new 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();
}
@Override
public void updateFileNameAndUID(String uid) throws ContactsStorageException {
String newFileName = uid + ".vcf";
ContentValues values = new ContentValues(2);
values.put(COLUMN_FILENAME, newFileName);
values.put(COLUMN_UID, uid);
update(values);
fileName = newFileName;
}
@Override
protected ContentValues contentValues() {
ContentValues values = super.contentValues();
@Cleanup("recycle") Parcel members = Parcel.obtain();
members.writeStringList(contact.members);
values.put(COLUMN_PENDING_MEMBERS, members.marshall());
return values;
}
/**
* Marks all members of the current group as dirty.
*/
public void markMembersDirty() throws ContactsStorageException {
assertID();
BatchOperation batch = new BatchOperation(addressBook.provider);
for (long member : getMembers())
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
.withValue(RawContacts.DIRTY, 1)
.withYieldAllowed(true)
));
batch.commit();
}
/**
* 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
*/
public static void applyPendingMemberships(LocalAddressBook addressBook) throws ContactsStorageException {
try {
@Cleanup Cursor cursor = addressBook.provider.query(
addressBook.syncAdapterURI(Groups.CONTENT_URI),
new String[] { Groups._ID, COLUMN_PENDING_MEMBERS },
COLUMN_PENDING_MEMBERS + " IS NOT NULL", new String[] {},
null
);
BatchOperation batch = new BatchOperation(addressBook.provider);
while (cursor != null && cursor.moveToNext()) {
long id = cursor.getLong(0);
App.log.fine("Assigning members to group " + id);
// delete all memberships and cached memberships for this group
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newDelete(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
.withSelection(
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" +
CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id), CachedGroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) })
.withYieldAllowed(true)
));
// extract list of member UIDs
List<String> members = new LinkedList<>();
byte[] raw = cursor.getBlob(1);
@Cleanup("recycle") Parcel parcel = Parcel.obtain();
parcel.unmarshall(raw, 0, raw.length);
parcel.setDataPosition(0);
parcel.readStringList(members);
// insert memberships
for (String uid : members) {
App.log.fine("Assigning member: " + uid);
try {
LocalContact member = addressBook.findContactByUID(uid);
member.addToGroup(batch, id);
} catch(FileNotFoundException e) {
App.log.log(Level.WARNING, "Group member not found: " + uid, e);
}
}
// remove pending memberships
batch.enqueue(new BatchOperation.Operation(
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
.withValue(COLUMN_PENDING_MEMBERS, null)
.withYieldAllowed(true)
));
batch.commit();
}
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't get pending memberships", e);
}
}
// helpers
private void assertID() {
if (id == null)
throw new IllegalStateException("Group has not been saved yet");
}
/**
* Lists all members of this group.
* @return list of all members' raw contact IDs
* @throws ContactsStorageException on contact provider errors
*/
protected long[] getMembers() throws ContactsStorageException {
assertID();
List<Long> members = new LinkedList<>();
try {
@Cleanup Cursor cursor = addressBook.provider.query(
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
new String[] { Data.RAW_CONTACT_ID },
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(id) },
null
);
while (cursor != null && cursor.moveToNext())
members.add(cursor.getLong(0));
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't list group members", e);
}
return ArrayUtils.toPrimitive(members.toArray(new Long[members.size()]));
}
// factory
static class Factory extends AndroidGroupFactory {
static final Factory INSTANCE = new Factory();
@Override
public LocalGroup newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) {
return new LocalGroup(addressBook, id, fileName, eTag);
}
@Override
public LocalGroup newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) {
return new LocalGroup(addressBook, contact, fileName, eTag);
}
@Override
public LocalGroup[] newArray(int size) {
return new LocalGroup[size];
}
}
}