mirror of
https://github.com/etesync/android
synced 2024-12-28 01:18:33 +00:00
Refactoring
* add overflow marker "…" to LoggingInputStream * split the populate() methods * always close cursors (resolves #120) * introduce LocalStorageException and its subclass RecordNotFoundException and catch RemoteException earlier * split synchronize() method into phases * commit after every added/modified record (obsoletes MAX_UPDATES_BEFORE_COMMIT) * read contact photos from (high-res) asset instead of the thumbnail blob (resolves #121)
This commit is contained in:
parent
6cfaad35b1
commit
8a651f135b
@ -14,12 +14,15 @@ public class LoggingInputStream extends FilterInputStream {
|
|||||||
|
|
||||||
ByteArrayOutputStream log = new ByteArrayOutputStream(MAX_LENGTH);
|
ByteArrayOutputStream log = new ByteArrayOutputStream(MAX_LENGTH);
|
||||||
int logSize = 0;
|
int logSize = 0;
|
||||||
|
boolean overflow = false;
|
||||||
|
|
||||||
|
|
||||||
public LoggingInputStream(String tag, InputStream proxy) {
|
public LoggingInputStream(String tag, InputStream proxy) {
|
||||||
super(proxy);
|
super(proxy);
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean markSupported() {
|
public boolean markSupported() {
|
||||||
return false;
|
return false;
|
||||||
@ -31,7 +34,8 @@ public class LoggingInputStream extends FilterInputStream {
|
|||||||
if (logSize < MAX_LENGTH) {
|
if (logSize < MAX_LENGTH) {
|
||||||
log.write(b);
|
log.write(b);
|
||||||
logSize++;
|
logSize++;
|
||||||
}
|
} else
|
||||||
|
overflow = true;
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,8 +44,10 @@ public class LoggingInputStream extends FilterInputStream {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
int read = super.read(buffer, byteOffset, byteCount);
|
int read = super.read(buffer, byteOffset, byteCount);
|
||||||
int bytesToLog = read;
|
int bytesToLog = read;
|
||||||
if (bytesToLog + logSize > MAX_LENGTH)
|
if (bytesToLog + logSize > MAX_LENGTH) {
|
||||||
bytesToLog = MAX_LENGTH - logSize;
|
bytesToLog = MAX_LENGTH - logSize;
|
||||||
|
overflow = true;
|
||||||
|
}
|
||||||
if (bytesToLog > 0) {
|
if (bytesToLog > 0) {
|
||||||
log.write(buffer, byteOffset, bytesToLog);
|
log.write(buffer, byteOffset, bytesToLog);
|
||||||
logSize += bytesToLog;
|
logSize += bytesToLog;
|
||||||
@ -51,7 +57,7 @@ public class LoggingInputStream extends FilterInputStream {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
Log.d(tag, "Content: " + log.toString());
|
Log.d(tag, "Content: " + log.toString() + (overflow ? "…" : ""));
|
||||||
super.close();
|
super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package at.bitfire.davdroid.resource;
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -15,6 +17,9 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
import org.apache.commons.lang.WordUtils;
|
import org.apache.commons.lang.WordUtils;
|
||||||
|
|
||||||
@ -24,6 +29,7 @@ import android.content.ContentProviderClient;
|
|||||||
import android.content.ContentProviderOperation;
|
import android.content.ContentProviderOperation;
|
||||||
import android.content.ContentProviderOperation.Builder;
|
import android.content.ContentProviderOperation.Builder;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
|
import android.content.res.AssetFileDescriptor;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.DatabaseUtils;
|
import android.database.DatabaseUtils;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -107,28 +113,68 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* content provider (= database) querying */
|
/* create/update/delete */
|
||||||
|
|
||||||
|
public Contact newResource(long localID, String resourceName, String eTag) {
|
||||||
|
return new Contact(localID, resourceName, eTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
||||||
|
String where;
|
||||||
|
|
||||||
|
if (remoteResources.length != 0) {
|
||||||
|
List<String> sqlFileNames = new LinkedList<String>();
|
||||||
|
for (Resource res : remoteResources)
|
||||||
|
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
||||||
|
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
|
||||||
|
} else
|
||||||
|
where = entryColumnRemoteName() + " IS NOT NULL";
|
||||||
|
|
||||||
|
Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null);
|
||||||
|
pendingOperations.add(builder
|
||||||
|
.withYieldAllowed(true)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* methods for populating the data object from the content provider */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void populate(Resource res) throws RemoteException {
|
public void populate(Resource res) throws LocalStorageException {
|
||||||
Contact c = (Contact)res;
|
Contact c = (Contact)res;
|
||||||
if (c.isPopulated())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
|
try {
|
||||||
new String[] { entryColumnUID(), RawContacts.STARRED }, null, null, null);
|
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()),
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
new String[] { entryColumnUID(), RawContacts.STARRED }, null, null, null);
|
||||||
c.setUid(cursor.getString(0));
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
c.setStarred(cursor.getInt(1) != 0);
|
c.setUid(cursor.getString(0));
|
||||||
|
c.setStarred(cursor.getInt(1) != 0);
|
||||||
|
} else
|
||||||
|
throw new RecordNotFoundException();
|
||||||
|
|
||||||
|
populateStructuredName(c);
|
||||||
|
populatePhoneNumbers(c);
|
||||||
|
populateEmailAddresses(c);
|
||||||
|
populatePhoto(c);
|
||||||
|
populateOrganization(c);
|
||||||
|
populateIMPPs(c);
|
||||||
|
populateNickname(c);
|
||||||
|
populateNote(c);
|
||||||
|
populatePostalAddresses(c);
|
||||||
|
populateURLs(c);
|
||||||
|
populateEvents(c);
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// structured name
|
private void populateStructuredName(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] {
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] {
|
||||||
/* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME,
|
/* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME,
|
||||||
/* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX,
|
/* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX,
|
||||||
/* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME
|
/* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME
|
||||||
}, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
}, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(res.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null);
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
c.setDisplayName(cursor.getString(0));
|
c.setDisplayName(cursor.getString(0));
|
||||||
|
|
||||||
@ -142,9 +188,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
c.setPhoneticMiddleName(cursor.getString(7));
|
c.setPhoneticMiddleName(cursor.getString(7));
|
||||||
c.setPhoneticFamilyName(cursor.getString(8));
|
c.setPhoneticFamilyName(cursor.getString(8));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// phone numbers
|
protected void populatePhoneNumbers(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER },
|
||||||
Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
@ -218,9 +265,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
}
|
}
|
||||||
c.getPhoneNumbers().add(number);
|
c.getPhoneNumbers().add(number);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// email addresses
|
protected void populateEmailAddresses(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL },
|
||||||
Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
@ -239,20 +287,26 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
String customType = cursor.getString(2);
|
String customType = cursor.getString(2);
|
||||||
if (customType != null && !customType.isEmpty())
|
if (customType != null && !customType.isEmpty())
|
||||||
email.addType(EmailType.get(labelToXName(customType)));
|
email.addType(EmailType.get(labelToXName(customType)));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
c.getEmails().add(email);
|
c.getEmails().add(email);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// photo
|
protected void populatePhoto(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Photo.PHOTO },
|
Uri photoUri = Uri.withAppendedPath(
|
||||||
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
ContentUris.withAppendedId(RawContacts.CONTENT_URI, c.getLocalID()),
|
||||||
new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null);
|
RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
|
||||||
if (cursor != null && cursor.moveToNext())
|
try {
|
||||||
c.setPhoto(cursor.getBlob(0));
|
@Cleanup AssetFileDescriptor fd = providerClient.openAssetFile(photoUri, "r");
|
||||||
|
@Cleanup InputStream is = fd.createInputStream();
|
||||||
|
c.setPhoto(IOUtils.toByteArray(is));
|
||||||
|
} catch(IOException ex) {
|
||||||
|
Log.w(TAG, "Couldn't read contact photo", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// organization
|
protected void populateOrganization(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Organization.COMPANY, Organization.TITLE },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Organization.COMPANY, Organization.TITLE },
|
||||||
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null);
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
@ -263,9 +317,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
if (role != null && !role.isEmpty())
|
if (role != null && !role.isEmpty())
|
||||||
c.setRole(role);
|
c.setRole(role);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IMPPs
|
protected void populateIMPPs(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL },
|
||||||
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
@ -302,7 +357,6 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
break;
|
break;
|
||||||
case Im.PROTOCOL_CUSTOM:
|
case Im.PROTOCOL_CUSTOM:
|
||||||
impp = new Impp(cursor.getString(4), handle);
|
impp = new Impp(cursor.getString(4), handle);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (impp != null) {
|
if (impp != null) {
|
||||||
@ -317,28 +371,30 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
String customType = cursor.getString(2);
|
String customType = cursor.getString(2);
|
||||||
if (customType != null && !customType.isEmpty())
|
if (customType != null && !customType.isEmpty())
|
||||||
impp.addType(ImppType.get(labelToXName(customType)));
|
impp.addType(ImppType.get(labelToXName(customType)));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
c.getImpps().add(impp);
|
c.getImpps().add(impp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nick name (max. 1)
|
protected void populateNickname(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME },
|
||||||
Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Nickname.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Nickname.CONTENT_ITEM_TYPE }, null);
|
||||||
if (cursor != null && cursor.moveToNext())
|
if (cursor != null && cursor.moveToNext())
|
||||||
c.setNickName(cursor.getString(0));
|
c.setNickName(cursor.getString(0));
|
||||||
|
}
|
||||||
|
|
||||||
// note (max. 1)
|
protected void populateNote(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Note.NOTE },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Note.NOTE },
|
||||||
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Note.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Note.CONTENT_ITEM_TYPE }, null);
|
||||||
if (cursor != null && cursor.moveToNext())
|
if (cursor != null && cursor.moveToNext())
|
||||||
c.setNote(cursor.getString(0));
|
c.setNote(cursor.getString(0));
|
||||||
|
}
|
||||||
|
|
||||||
// postal addresses
|
protected void populatePostalAddresses(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] {
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] {
|
||||||
/* 0 */ StructuredPostal.FORMATTED_ADDRESS, StructuredPostal.TYPE, StructuredPostal.LABEL,
|
/* 0 */ StructuredPostal.FORMATTED_ADDRESS, StructuredPostal.TYPE, StructuredPostal.LABEL,
|
||||||
/* 3 */ StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
|
/* 3 */ StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
|
||||||
/* 6 */ StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
|
/* 6 */ StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
|
||||||
@ -371,16 +427,18 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
address.setCountry(cursor.getString(9));
|
address.setCountry(cursor.getString(9));
|
||||||
c.getAddresses().add(address);
|
c.getAddresses().add(address);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// URL
|
protected void populateURLs(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { Website.URL },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { Website.URL },
|
||||||
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), Website.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), Website.CONTENT_ITEM_TYPE }, null);
|
||||||
if (cursor != null && cursor.moveToNext())
|
if (cursor != null && cursor.moveToNext())
|
||||||
c.getURLs().add(cursor.getString(0));
|
c.getURLs().add(cursor.getString(0));
|
||||||
|
}
|
||||||
|
|
||||||
// events
|
protected void populateEvents(Contact c) throws RemoteException {
|
||||||
cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE },
|
@Cleanup("close") Cursor cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE },
|
||||||
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
|
||||||
new String[] { String.valueOf(c.getLocalID()), CommonDataKinds.Event.CONTENT_ITEM_TYPE }, null);
|
new String[] { String.valueOf(c.getLocalID()), CommonDataKinds.Event.CONTENT_ITEM_TYPE }, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
@ -399,63 +457,6 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
Log.w(TAG, "Couldn't parse local birthday/anniversary date", e);
|
Log.w(TAG, "Couldn't parse local birthday/anniversary date", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.populated = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
|
||||||
String where;
|
|
||||||
|
|
||||||
if (remoteResources.length != 0) {
|
|
||||||
List<String> sqlFileNames = new LinkedList<String>();
|
|
||||||
for (Resource res : remoteResources)
|
|
||||||
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
|
||||||
where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")";
|
|
||||||
} else
|
|
||||||
where = entryColumnRemoteName() + " IS NOT NULL";
|
|
||||||
|
|
||||||
Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null);
|
|
||||||
pendingOperations.add(builder
|
|
||||||
.withYieldAllowed(true)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* create/update/delete */
|
|
||||||
|
|
||||||
public Contact newResource(long localID, String resourceName, String eTag) {
|
|
||||||
return new Contact(localID, resourceName, eTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* private helper methods */
|
|
||||||
|
|
||||||
protected Uri dataURI() {
|
|
||||||
return syncAdapterURI(Data.CONTENT_URI);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String labelToXName(String label) {
|
|
||||||
if (label == null)
|
|
||||||
return null;
|
|
||||||
String xName = "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US);
|
|
||||||
return xName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
|
|
||||||
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String xNameToLabel(String xname) {
|
|
||||||
if (xname == null)
|
|
||||||
return null;
|
|
||||||
// "x-my_property"
|
|
||||||
// 1. ensure lower case -> "x-my_property"
|
|
||||||
// 2. remove x- from beginning -> "my_property"
|
|
||||||
// 3. replace "_" by " " -> "my property"
|
|
||||||
// 4. capitalize -> "My Property"
|
|
||||||
return WordUtils.capitalize(StringUtils.removeStart(xname.toLowerCase(Locale.US), "x-").replaceAll("_"," "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -788,4 +789,32 @@ public class LocalAddressBook extends LocalCollection<Contact> {
|
|||||||
.withValue(CommonDataKinds.Event.TYPE, type)
|
.withValue(CommonDataKinds.Event.TYPE, type)
|
||||||
.withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate()));
|
.withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* helper methods */
|
||||||
|
|
||||||
|
protected Uri dataURI() {
|
||||||
|
return syncAdapterURI(Data.CONTENT_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String labelToXName(String label) {
|
||||||
|
return "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) {
|
||||||
|
return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String xNameToLabel(String xname) {
|
||||||
|
if (xname == null)
|
||||||
|
return null;
|
||||||
|
// "x-my_property"
|
||||||
|
// 1. ensure lower case -> "x-my_property"
|
||||||
|
// 2. remove x- from beginning -> "my_property"
|
||||||
|
// 3. replace "_" by " " -> "my property"
|
||||||
|
// 4. capitalize -> "My Property"
|
||||||
|
return WordUtils.capitalize(StringUtils.removeStart(xname.toLowerCase(Locale.US), "x-").replaceAll("_"," "));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import java.util.List;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import net.fortuna.ical4j.model.Dur;
|
import net.fortuna.ical4j.model.Dur;
|
||||||
import net.fortuna.ical4j.model.Parameter;
|
import net.fortuna.ical4j.model.Parameter;
|
||||||
@ -137,7 +138,7 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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),
|
@Cleanup("close") Cursor cursor = providerClient.query(calendarsURI(account),
|
||||||
new String[] { Calendars._ID, Calendars.NAME, COLLECTION_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);
|
||||||
|
|
||||||
@ -165,200 +166,12 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* content provider (= database) querying */
|
/* create/update/delete */
|
||||||
|
|
||||||
@Override
|
public Event newResource(long localID, String resourceName, String eTag) {
|
||||||
public void populate(Resource resource) throws RemoteException {
|
return new Event(localID, resourceName, eTag);
|
||||||
Event e = (Event)resource;
|
|
||||||
if (e.isPopulated())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()),
|
|
||||||
new String[] {
|
|
||||||
/* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION,
|
|
||||||
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
|
|
||||||
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
|
|
||||||
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
|
|
||||||
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
|
|
||||||
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
|
|
||||||
}, null, null, null);
|
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
|
||||||
e.setUid(cursor.getString(17));
|
|
||||||
|
|
||||||
e.setSummary(cursor.getString(0));
|
|
||||||
e.setLocation(cursor.getString(1));
|
|
||||||
e.setDescription(cursor.getString(2));
|
|
||||||
|
|
||||||
long tsStart = cursor.getLong(3),
|
|
||||||
tsEnd = cursor.getLong(4);
|
|
||||||
|
|
||||||
String tzId;
|
|
||||||
if (cursor.getInt(7) != 0) { // ALL_DAY != 0
|
|
||||||
tzId = null; // -> use UTC
|
|
||||||
} else {
|
|
||||||
// use the start time zone for the end time, too
|
|
||||||
// because the Samsung Planner UI allows the user to change the time zone
|
|
||||||
// but it will change the start time zone only
|
|
||||||
tzId = cursor.getString(5);
|
|
||||||
//tzIdEnd = cursor.getString(6);
|
|
||||||
}
|
|
||||||
e.setDtStart(tsStart, tzId);
|
|
||||||
if (tsEnd != 0)
|
|
||||||
e.setDtEnd(tsEnd, tzId);
|
|
||||||
|
|
||||||
// recurrence
|
|
||||||
try {
|
|
||||||
String duration = cursor.getString(18);
|
|
||||||
if (duration != null && !duration.isEmpty())
|
|
||||||
e.setDuration(new Duration(new Dur(duration)));
|
|
||||||
|
|
||||||
String strRRule = cursor.getString(10);
|
|
||||||
if (strRRule != null && !strRRule.isEmpty())
|
|
||||||
e.setRrule(new RRule(strRRule));
|
|
||||||
|
|
||||||
String strRDate = cursor.getString(11);
|
|
||||||
if (strRDate != null && !strRDate.isEmpty()) {
|
|
||||||
RDate rDate = new RDate();
|
|
||||||
rDate.setValue(strRDate);
|
|
||||||
e.setRdate(rDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
String strExRule = cursor.getString(12);
|
|
||||||
if (strExRule != null && !strExRule.isEmpty()) {
|
|
||||||
ExRule exRule = new ExRule();
|
|
||||||
exRule.setValue(strExRule);
|
|
||||||
e.setExrule(exRule);
|
|
||||||
}
|
|
||||||
|
|
||||||
String strExDate = cursor.getString(13);
|
|
||||||
if (strExDate != null && !strExDate.isEmpty()) {
|
|
||||||
// ignored, see https://code.google.com/p/android/issues/detail?id=21426
|
|
||||||
ExDate exDate = new ExDate();
|
|
||||||
exDate.setValue(strExDate);
|
|
||||||
e.setExdate(exDate);
|
|
||||||
}
|
|
||||||
} catch (ParseException ex) {
|
|
||||||
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// status
|
|
||||||
switch (cursor.getInt(8)) {
|
|
||||||
case Events.STATUS_CONFIRMED:
|
|
||||||
e.setStatus(Status.VEVENT_CONFIRMED);
|
|
||||||
break;
|
|
||||||
case Events.STATUS_TENTATIVE:
|
|
||||||
e.setStatus(Status.VEVENT_TENTATIVE);
|
|
||||||
break;
|
|
||||||
case Events.STATUS_CANCELED:
|
|
||||||
e.setStatus(Status.VEVENT_CANCELLED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// availability
|
|
||||||
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
|
|
||||||
|
|
||||||
// attendees
|
|
||||||
if (cursor.getInt(14) != 0) { // has attendees
|
|
||||||
try {
|
|
||||||
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
|
|
||||||
} catch (URISyntaxException ex) {
|
|
||||||
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
|
|
||||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
|
||||||
.build();
|
|
||||||
Cursor c = providerClient.query(attendeesUri, new String[] {
|
|
||||||
/* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE,
|
|
||||||
/* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS
|
|
||||||
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
|
|
||||||
while (c != null && c.moveToNext()) {
|
|
||||||
try {
|
|
||||||
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
|
|
||||||
ParameterList params = attendee.getParameters();
|
|
||||||
|
|
||||||
String cn = c.getString(1);
|
|
||||||
if (cn != null)
|
|
||||||
params.add(new Cn(cn));
|
|
||||||
|
|
||||||
// type
|
|
||||||
int type = c.getInt(2);
|
|
||||||
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
|
|
||||||
|
|
||||||
// role
|
|
||||||
int relationship = c.getInt(3);
|
|
||||||
switch (relationship) {
|
|
||||||
case Attendees.RELATIONSHIP_ORGANIZER:
|
|
||||||
params.add(Role.CHAIR);
|
|
||||||
break;
|
|
||||||
case Attendees.RELATIONSHIP_ATTENDEE:
|
|
||||||
case Attendees.RELATIONSHIP_PERFORMER:
|
|
||||||
case Attendees.RELATIONSHIP_SPEAKER:
|
|
||||||
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
|
|
||||||
break;
|
|
||||||
case Attendees.RELATIONSHIP_NONE:
|
|
||||||
params.add(Role.NON_PARTICIPANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// status
|
|
||||||
switch (c.getInt(4)) {
|
|
||||||
case Attendees.ATTENDEE_STATUS_INVITED:
|
|
||||||
params.add(PartStat.NEEDS_ACTION);
|
|
||||||
break;
|
|
||||||
case Attendees.ATTENDEE_STATUS_ACCEPTED:
|
|
||||||
params.add(PartStat.ACCEPTED);
|
|
||||||
break;
|
|
||||||
case Attendees.ATTENDEE_STATUS_DECLINED:
|
|
||||||
params.add(PartStat.DECLINED);
|
|
||||||
break;
|
|
||||||
case Attendees.ATTENDEE_STATUS_TENTATIVE:
|
|
||||||
params.add(PartStat.TENTATIVE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.addAttendee(attendee);
|
|
||||||
} catch (URISyntaxException ex) {
|
|
||||||
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// classification
|
|
||||||
switch (cursor.getInt(9)) {
|
|
||||||
case Events.ACCESS_CONFIDENTIAL:
|
|
||||||
case Events.ACCESS_PRIVATE:
|
|
||||||
e.setForPublic(false);
|
|
||||||
break;
|
|
||||||
case Events.ACCESS_PUBLIC:
|
|
||||||
e.setForPublic(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reminders
|
|
||||||
Uri remindersUri = Reminders.CONTENT_URI.buildUpon()
|
|
||||||
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
|
||||||
.build();
|
|
||||||
Cursor c = providerClient.query(remindersUri, new String[] {
|
|
||||||
/* 0 */ Reminders.MINUTES, Reminders.METHOD
|
|
||||||
}, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
|
|
||||||
while (c != null && c.moveToNext()) {
|
|
||||||
VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0));
|
|
||||||
|
|
||||||
PropertyList props = alarm.getProperties();
|
|
||||||
switch (c.getInt(1)) {
|
|
||||||
/*case Reminders.METHOD_EMAIL:
|
|
||||||
props.add(Action.EMAIL);
|
|
||||||
break;*/
|
|
||||||
default:
|
|
||||||
props.add(Action.DISPLAY);
|
|
||||||
props.add(new Description(e.getSummary()));
|
|
||||||
}
|
|
||||||
e.addAlarm(alarm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
public void deleteAllExceptRemoteNames(Resource[] remoteResources) {
|
||||||
String where;
|
String where;
|
||||||
|
|
||||||
@ -378,25 +191,210 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* create/update/delete */
|
/* methods for populating the data object from the content provider */
|
||||||
|
|
||||||
public Event newResource(long localID, String resourceName, String eTag) {
|
|
||||||
return new Event(localID, resourceName, eTag);
|
@Override
|
||||||
|
public void populate(Resource resource) throws LocalStorageException {
|
||||||
|
Event e = (Event)resource;
|
||||||
|
|
||||||
|
try {
|
||||||
|
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()),
|
||||||
|
new String[] {
|
||||||
|
/* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION,
|
||||||
|
/* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY,
|
||||||
|
/* 8 */ Events.STATUS, Events.ACCESS_LEVEL,
|
||||||
|
/* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE,
|
||||||
|
/* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS,
|
||||||
|
/* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY
|
||||||
|
}, null, null, null);
|
||||||
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
|
e.setUid(cursor.getString(17));
|
||||||
|
|
||||||
|
e.setSummary(cursor.getString(0));
|
||||||
|
e.setLocation(cursor.getString(1));
|
||||||
|
e.setDescription(cursor.getString(2));
|
||||||
|
|
||||||
|
long tsStart = cursor.getLong(3),
|
||||||
|
tsEnd = cursor.getLong(4);
|
||||||
|
|
||||||
|
String tzId;
|
||||||
|
if (cursor.getInt(7) != 0) { // ALL_DAY != 0
|
||||||
|
tzId = null; // -> use UTC
|
||||||
|
} else {
|
||||||
|
// use the start time zone for the end time, too
|
||||||
|
// because the Samsung Planner UI allows the user to change the time zone
|
||||||
|
// but it will change the start time zone only
|
||||||
|
tzId = cursor.getString(5);
|
||||||
|
//tzIdEnd = cursor.getString(6);
|
||||||
|
}
|
||||||
|
e.setDtStart(tsStart, tzId);
|
||||||
|
if (tsEnd != 0)
|
||||||
|
e.setDtEnd(tsEnd, tzId);
|
||||||
|
|
||||||
|
// recurrence
|
||||||
|
try {
|
||||||
|
String duration = cursor.getString(18);
|
||||||
|
if (duration != null && !duration.isEmpty())
|
||||||
|
e.setDuration(new Duration(new Dur(duration)));
|
||||||
|
|
||||||
|
String strRRule = cursor.getString(10);
|
||||||
|
if (strRRule != null && !strRRule.isEmpty())
|
||||||
|
e.setRrule(new RRule(strRRule));
|
||||||
|
|
||||||
|
String strRDate = cursor.getString(11);
|
||||||
|
if (strRDate != null && !strRDate.isEmpty()) {
|
||||||
|
RDate rDate = new RDate();
|
||||||
|
rDate.setValue(strRDate);
|
||||||
|
e.setRdate(rDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
String strExRule = cursor.getString(12);
|
||||||
|
if (strExRule != null && !strExRule.isEmpty()) {
|
||||||
|
ExRule exRule = new ExRule();
|
||||||
|
exRule.setValue(strExRule);
|
||||||
|
e.setExrule(exRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
String strExDate = cursor.getString(13);
|
||||||
|
if (strExDate != null && !strExDate.isEmpty()) {
|
||||||
|
// ignored, see https://code.google.com/p/android/issues/detail?id=21426
|
||||||
|
ExDate exDate = new ExDate();
|
||||||
|
exDate.setValue(strExDate);
|
||||||
|
e.setExdate(exDate);
|
||||||
|
}
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
Log.w(TAG, "Invalid recurrence rules, ignoring", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// status
|
||||||
|
switch (cursor.getInt(8)) {
|
||||||
|
case Events.STATUS_CONFIRMED:
|
||||||
|
e.setStatus(Status.VEVENT_CONFIRMED);
|
||||||
|
break;
|
||||||
|
case Events.STATUS_TENTATIVE:
|
||||||
|
e.setStatus(Status.VEVENT_TENTATIVE);
|
||||||
|
break;
|
||||||
|
case Events.STATUS_CANCELED:
|
||||||
|
e.setStatus(Status.VEVENT_CANCELLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// availability
|
||||||
|
e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE);
|
||||||
|
|
||||||
|
// attendees
|
||||||
|
if (cursor.getInt(14) != 0) { // has attendees
|
||||||
|
try {
|
||||||
|
e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null)));
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex);
|
||||||
|
}
|
||||||
|
populateAttendees(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// classification
|
||||||
|
switch (cursor.getInt(9)) {
|
||||||
|
case Events.ACCESS_CONFIDENTIAL:
|
||||||
|
case Events.ACCESS_PRIVATE:
|
||||||
|
e.setForPublic(false);
|
||||||
|
break;
|
||||||
|
case Events.ACCESS_PUBLIC:
|
||||||
|
e.setForPublic(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
populateReminders(e);
|
||||||
|
} else
|
||||||
|
throw new RecordNotFoundException();
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* private helper methods */
|
void populateAttendees(Event e) throws RemoteException {
|
||||||
|
Uri attendeesUri = Attendees.CONTENT_URI.buildUpon()
|
||||||
|
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
|
.build();
|
||||||
|
@Cleanup("close") Cursor c = providerClient.query(attendeesUri, new String[] {
|
||||||
|
/* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE,
|
||||||
|
/* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS
|
||||||
|
}, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
|
||||||
|
while (c != null && c.moveToNext()) {
|
||||||
|
try {
|
||||||
|
Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null));
|
||||||
|
ParameterList params = attendee.getParameters();
|
||||||
|
|
||||||
protected static Uri calendarsURI(Account account) {
|
String cn = c.getString(1);
|
||||||
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
if (cn != null)
|
||||||
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
params.add(new Cn(cn));
|
||||||
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
|
|
||||||
|
// type
|
||||||
|
int type = c.getInt(2);
|
||||||
|
params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL);
|
||||||
|
|
||||||
|
// role
|
||||||
|
int relationship = c.getInt(3);
|
||||||
|
switch (relationship) {
|
||||||
|
case Attendees.RELATIONSHIP_ORGANIZER:
|
||||||
|
params.add(Role.CHAIR);
|
||||||
|
break;
|
||||||
|
case Attendees.RELATIONSHIP_ATTENDEE:
|
||||||
|
case Attendees.RELATIONSHIP_PERFORMER:
|
||||||
|
case Attendees.RELATIONSHIP_SPEAKER:
|
||||||
|
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
|
||||||
|
break;
|
||||||
|
case Attendees.RELATIONSHIP_NONE:
|
||||||
|
params.add(Role.NON_PARTICIPANT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// status
|
||||||
|
switch (c.getInt(4)) {
|
||||||
|
case Attendees.ATTENDEE_STATUS_INVITED:
|
||||||
|
params.add(PartStat.NEEDS_ACTION);
|
||||||
|
break;
|
||||||
|
case Attendees.ATTENDEE_STATUS_ACCEPTED:
|
||||||
|
params.add(PartStat.ACCEPTED);
|
||||||
|
break;
|
||||||
|
case Attendees.ATTENDEE_STATUS_DECLINED:
|
||||||
|
params.add(PartStat.DECLINED);
|
||||||
|
break;
|
||||||
|
case Attendees.ATTENDEE_STATUS_TENTATIVE:
|
||||||
|
params.add(PartStat.TENTATIVE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.addAttendee(attendee);
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Uri calendarsURI() {
|
void populateReminders(Event e) throws RemoteException {
|
||||||
return calendarsURI(account);
|
// reminders
|
||||||
}
|
Uri remindersUri = Reminders.CONTENT_URI.buildUpon()
|
||||||
|
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
||||||
|
.build();
|
||||||
|
@Cleanup("close") Cursor c = providerClient.query(remindersUri, new String[] {
|
||||||
|
/* 0 */ Reminders.MINUTES, Reminders.METHOD
|
||||||
|
}, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null);
|
||||||
|
while (c != null && c.moveToNext()) {
|
||||||
|
VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0));
|
||||||
|
|
||||||
|
PropertyList props = alarm.getProperties();
|
||||||
|
switch (c.getInt(1)) {
|
||||||
|
/*case Reminders.METHOD_EMAIL:
|
||||||
|
props.add(Action.EMAIL);
|
||||||
|
break;*/
|
||||||
|
default:
|
||||||
|
props.add(Action.DISPLAY);
|
||||||
|
props.add(new Description(e.getSummary()));
|
||||||
|
}
|
||||||
|
e.addAlarm(alarm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* content builder methods */
|
/* content builder methods */
|
||||||
@ -570,4 +568,19 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
|
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
|
||||||
.withValue(Reminders.MINUTES, minutes);
|
.withValue(Reminders.MINUTES, minutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* private helper methods */
|
||||||
|
|
||||||
|
protected static Uri calendarsURI(Account account) {
|
||||||
|
return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name)
|
||||||
|
.appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type)
|
||||||
|
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Uri calendarsURI() {
|
||||||
|
return calendarsURI(account);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ package at.bitfire.davdroid.resource;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import lombok.Cleanup;
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentProviderOperation;
|
import android.content.ContentProviderOperation;
|
||||||
@ -63,91 +64,111 @@ public abstract class LocalCollection<T extends Resource> {
|
|||||||
|
|
||||||
// content provider (= database) querying
|
// content provider (= database) querying
|
||||||
|
|
||||||
public Resource[] findDirty() throws RemoteException {
|
public Resource[] findDirty() throws LocalStorageException {
|
||||||
String where = entryColumnDirty() + "=1";
|
String where = entryColumnDirty() + "=1";
|
||||||
if (entryColumnParentID() != null)
|
if (entryColumnParentID() != null)
|
||||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
try {
|
||||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
|
||||||
where, null, null);
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
LinkedList<T> dirty = new LinkedList<T>();
|
where, null, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
LinkedList<T> dirty = new LinkedList<T>();
|
||||||
T resource = findById(cursor.getLong(0), true);
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
if (resource != null)
|
T resource = findById(cursor.getLong(0), true);
|
||||||
dirty.add(resource);
|
if (resource != null)
|
||||||
|
dirty.add(resource);
|
||||||
|
}
|
||||||
|
return dirty.toArray(new Resource[0]);
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
}
|
}
|
||||||
return dirty.toArray(new Resource[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource[] findDeleted() throws RemoteException {
|
public Resource[] findDeleted() throws LocalStorageException {
|
||||||
String where = entryColumnDeleted() + "=1";
|
String where = entryColumnDeleted() + "=1";
|
||||||
if (entryColumnParentID() != null)
|
if (entryColumnParentID() != null)
|
||||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
try {
|
||||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
|
||||||
where, null, null);
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
LinkedList<T> deleted = new LinkedList<T>();
|
where, null, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
LinkedList<T> deleted = new LinkedList<T>();
|
||||||
T resource = findById(cursor.getLong(0), false);
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
if (resource != null)
|
T resource = findById(cursor.getLong(0), false);
|
||||||
deleted.add(resource);
|
if (resource != null)
|
||||||
|
deleted.add(resource);
|
||||||
|
}
|
||||||
|
return deleted.toArray(new Resource[0]);
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
}
|
}
|
||||||
return deleted.toArray(new Resource[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource[] findNew() throws RemoteException {
|
public Resource[] findNew() throws LocalStorageException {
|
||||||
String where = entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NULL";
|
String where = entryColumnDirty() + "=1 AND " + entryColumnRemoteName() + " IS NULL";
|
||||||
if (entryColumnParentID() != null)
|
if (entryColumnParentID() != null)
|
||||||
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId());
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
try {
|
||||||
new String[] { entryColumnID() },
|
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
|
||||||
where, null, null);
|
new String[] { entryColumnID() },
|
||||||
LinkedList<T> fresh = new LinkedList<T>();
|
where, null, null);
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
LinkedList<T> fresh = new LinkedList<T>();
|
||||||
T resource = findById(cursor.getLong(0), true);
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
if (resource != null) {
|
T resource = findById(cursor.getLong(0), true);
|
||||||
resource.initialize();
|
if (resource != null) {
|
||||||
|
resource.initialize();
|
||||||
|
|
||||||
// new record: set generated UID + remote file name in database
|
// new record: set generated UID + remote file name in database
|
||||||
pendingOperations.add(ContentProviderOperation
|
pendingOperations.add(ContentProviderOperation
|
||||||
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
.newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID()))
|
||||||
.withValue(entryColumnUID(), resource.getUid())
|
.withValue(entryColumnUID(), resource.getUid())
|
||||||
.withValue(entryColumnRemoteName(), resource.getName())
|
.withValue(entryColumnRemoteName(), resource.getName())
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
fresh.add(resource);
|
fresh.add(resource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return fresh.toArray(new Resource[0]);
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
}
|
}
|
||||||
return fresh.toArray(new Resource[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public T findById(long localID, boolean populate) throws RemoteException {
|
public T findById(long localID, boolean populate) throws LocalStorageException {
|
||||||
Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
|
try {
|
||||||
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
|
@Cleanup("close") Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID),
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null);
|
||||||
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
if (populate)
|
T resource = newResource(localID, cursor.getString(0), cursor.getString(1));
|
||||||
populate(resource);
|
if (populate)
|
||||||
return resource;
|
populate(resource);
|
||||||
} else
|
return resource;
|
||||||
return null;
|
} else
|
||||||
|
throw new RecordNotFoundException();
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public T findByRemoteName(String remoteName, boolean populate) throws RemoteException {
|
public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException {
|
||||||
Cursor cursor = providerClient.query(entriesURI(),
|
try {
|
||||||
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
@Cleanup("close") Cursor cursor = providerClient.query(entriesURI(),
|
||||||
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
|
new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() },
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
entryColumnRemoteName() + "=?", new String[] { remoteName }, null);
|
||||||
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
if (populate)
|
T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2));
|
||||||
populate(resource);
|
if (populate)
|
||||||
return resource;
|
populate(resource);
|
||||||
} else
|
return resource;
|
||||||
return null;
|
} else
|
||||||
|
throw new RecordNotFoundException();
|
||||||
|
} catch(RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public abstract void populate(Resource record) throws RemoteException;
|
public abstract void populate(Resource record) throws LocalStorageException;
|
||||||
|
|
||||||
protected void queueOperation(Builder builder) {
|
protected void queueOperation(Builder builder) {
|
||||||
if (builder != null)
|
if (builder != null)
|
||||||
@ -169,18 +190,16 @@ public abstract class LocalCollection<T extends Resource> {
|
|||||||
addDataRows(resource, -1, idx);
|
addDataRows(resource, -1, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateByRemoteName(Resource remoteResource) throws RemoteException {
|
public void updateByRemoteName(Resource remoteResource) throws LocalStorageException {
|
||||||
T localResource = findByRemoteName(remoteResource.getName(), false);
|
T localResource = findByRemoteName(remoteResource.getName(), false);
|
||||||
if (localResource != null) {
|
pendingOperations.add(
|
||||||
pendingOperations.add(
|
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
|
||||||
buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource)
|
.withValue(entryColumnETag(), remoteResource.getETag())
|
||||||
.withValue(entryColumnETag(), remoteResource.getETag())
|
.withYieldAllowed(true)
|
||||||
.withYieldAllowed(true)
|
.build());
|
||||||
.build());
|
|
||||||
|
|
||||||
removeDataRows(localResource);
|
removeDataRows(localResource);
|
||||||
addDataRows(remoteResource, localResource.getLocalID(), -1);
|
addDataRows(remoteResource, localResource.getLocalID(), -1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Resource resource) {
|
public void delete(Resource resource) {
|
||||||
@ -198,12 +217,17 @@ public abstract class LocalCollection<T extends Resource> {
|
|||||||
.withValue(entryColumnDirty(), 0).build());
|
.withValue(entryColumnDirty(), 0).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void commit() throws RemoteException, OperationApplicationException {
|
public void commit() throws LocalStorageException {
|
||||||
if (!pendingOperations.isEmpty()) {
|
if (!pendingOperations.isEmpty())
|
||||||
Log.i(TAG, "Committing " + pendingOperations.size() + " operations");
|
try {
|
||||||
providerClient.applyBatch(pendingOperations);
|
Log.i(TAG, "Committing " + pendingOperations.size() + " operations");
|
||||||
pendingOperations.clear();
|
providerClient.applyBatch(pendingOperations);
|
||||||
}
|
pendingOperations.clear();
|
||||||
|
} catch (RemoteException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
|
} catch(OperationApplicationException ex) {
|
||||||
|
throw new LocalStorageException(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
24
src/at/bitfire/davdroid/resource/LocalStorageException.java
Normal file
24
src/at/bitfire/davdroid/resource/LocalStorageException.java
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
public class LocalStorageException extends Exception {
|
||||||
|
private static final long serialVersionUID = -7787658815291629529L;
|
||||||
|
|
||||||
|
private static final String detailMessage = "Couldn't access local content provider";
|
||||||
|
|
||||||
|
|
||||||
|
public LocalStorageException(String detailMessage, Throwable throwable) {
|
||||||
|
super(detailMessage, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalStorageException(String detailMessage) {
|
||||||
|
super(detailMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalStorageException(Throwable throwable) {
|
||||||
|
super(detailMessage, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalStorageException() {
|
||||||
|
super(detailMessage);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package at.bitfire.davdroid.resource;
|
||||||
|
|
||||||
|
public class RecordNotFoundException extends LocalStorageException {
|
||||||
|
private static final long serialVersionUID = 4961024282198632578L;
|
||||||
|
|
||||||
|
private static final String detailMessage = "Record not found in local content provider";
|
||||||
|
|
||||||
|
|
||||||
|
RecordNotFoundException(Throwable ex) {
|
||||||
|
super(detailMessage, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordNotFoundException() {
|
||||||
|
super(detailMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -23,8 +23,6 @@ public abstract class Resource {
|
|||||||
@Getter @Setter protected String uid;
|
@Getter @Setter protected String uid;
|
||||||
@Getter protected long localID;
|
@Getter protected long localID;
|
||||||
|
|
||||||
@Getter protected boolean populated = false;
|
|
||||||
|
|
||||||
|
|
||||||
public Resource(String name, String ETag) {
|
public Resource(String name, String ETag) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -15,13 +15,12 @@ import android.content.AbstractThreadedSyncAdapter;
|
|||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.OperationApplicationException;
|
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import at.bitfire.davdroid.resource.LocalCollection;
|
import at.bitfire.davdroid.resource.LocalCollection;
|
||||||
|
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||||
import at.bitfire.davdroid.resource.RemoteCollection;
|
import at.bitfire.davdroid.resource.RemoteCollection;
|
||||||
import at.bitfire.davdroid.webdav.DAVException;
|
import at.bitfire.davdroid.webdav.DAVException;
|
||||||
|
|
||||||
@ -71,19 +70,16 @@ public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||||||
|
|
||||||
} catch (AuthenticationException ex) {
|
} catch (AuthenticationException ex) {
|
||||||
syncResult.stats.numAuthExceptions++;
|
syncResult.stats.numAuthExceptions++;
|
||||||
Log.e(TAG, "HTTP authorization error", ex);
|
Log.e(TAG, "HTTP authentication failed", ex);
|
||||||
} catch (DAVException ex) {
|
} catch (DAVException ex) {
|
||||||
syncResult.stats.numParseExceptions++;
|
syncResult.stats.numParseExceptions++;
|
||||||
Log.e(TAG, "Invalid DAV response", ex);
|
Log.e(TAG, "Invalid DAV response", ex);
|
||||||
} catch (HttpException ex) {
|
} catch (HttpException ex) {
|
||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
Log.e(TAG, "HTTP error", ex);
|
Log.e(TAG, "HTTP error", ex);
|
||||||
} catch (OperationApplicationException ex) {
|
} catch (LocalStorageException ex) {
|
||||||
syncResult.databaseError = true;
|
syncResult.databaseError = true;
|
||||||
Log.e(TAG, "Content provider operation error", ex);
|
Log.e(TAG, "Local storage (content provider) exception", ex);
|
||||||
} catch (RemoteException ex) {
|
|
||||||
syncResult.databaseError = true;
|
|
||||||
Log.e(TAG, "Remote process (content provider?) died", ex);
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
Log.e(TAG, "I/O error", ex);
|
Log.e(TAG, "I/O error", ex);
|
||||||
|
@ -17,11 +17,11 @@ import org.apache.http.HttpException;
|
|||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
import android.content.OperationApplicationException;
|
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.os.RemoteException;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import at.bitfire.davdroid.resource.LocalCollection;
|
import at.bitfire.davdroid.resource.LocalCollection;
|
||||||
|
import at.bitfire.davdroid.resource.LocalStorageException;
|
||||||
|
import at.bitfire.davdroid.resource.RecordNotFoundException;
|
||||||
import at.bitfire.davdroid.resource.RemoteCollection;
|
import at.bitfire.davdroid.resource.RemoteCollection;
|
||||||
import at.bitfire.davdroid.resource.Resource;
|
import at.bitfire.davdroid.resource.Resource;
|
||||||
import at.bitfire.davdroid.webdav.NotFoundException;
|
import at.bitfire.davdroid.webdav.NotFoundException;
|
||||||
@ -29,7 +29,6 @@ import at.bitfire.davdroid.webdav.PreconditionFailedException;
|
|||||||
|
|
||||||
public class SyncManager {
|
public class SyncManager {
|
||||||
private static final String TAG = "davdroid.SyncManager";
|
private static final String TAG = "davdroid.SyncManager";
|
||||||
private static final int MAX_UPDATES_BEFORE_COMMIT = 25;
|
|
||||||
|
|
||||||
protected Account account;
|
protected Account account;
|
||||||
protected AccountManager accountManager;
|
protected AccountManager accountManager;
|
||||||
@ -40,132 +39,149 @@ public class SyncManager {
|
|||||||
this.accountManager = accountManager;
|
this.accountManager = accountManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> dav, boolean manualSync, SyncResult syncResult) throws RemoteException, OperationApplicationException, IOException, HttpException {
|
public void synchronize(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, boolean manualSync, SyncResult syncResult) throws LocalStorageException, IOException, HttpException {
|
||||||
boolean fetchCollection = false;
|
// PHASE 1: push local changes to server
|
||||||
|
int deletedRemotely = pushDeleted(local, remote),
|
||||||
|
addedRemotely = pushNew(local, remote),
|
||||||
|
updatedRemotely = pushDirty(local, remote);
|
||||||
|
|
||||||
// PHASE 1: UPLOAD LOCALLY-CHANGED RESOURCES
|
syncResult.stats.numEntries = deletedRemotely + addedRemotely + updatedRemotely;
|
||||||
// remove deleted resources from remote
|
|
||||||
|
// PHASE 2A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed)
|
||||||
|
boolean fetchCollection = syncResult.stats.numEntries > 0;
|
||||||
|
if (manualSync) {
|
||||||
|
Log.i(TAG, "Synchronization forced");
|
||||||
|
fetchCollection = true;
|
||||||
|
}
|
||||||
|
if (!fetchCollection) {
|
||||||
|
String currentCTag = remote.getCTag(),
|
||||||
|
lastCTag = local.getCTag();
|
||||||
|
if (currentCTag == null || !currentCTag.equals(lastCTag))
|
||||||
|
fetchCollection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE 2B: detect details of remote changes
|
||||||
|
Log.i(TAG, "Fetching remote resource list");
|
||||||
|
Set<Resource> remotelyAdded = new HashSet<Resource>(),
|
||||||
|
remotelyUpdated = new HashSet<Resource>();
|
||||||
|
|
||||||
|
Resource[] remoteResources = remote.getMemberETags();
|
||||||
|
if (remoteResources != null) {
|
||||||
|
for (Resource remoteResource : remoteResources) {
|
||||||
|
try {
|
||||||
|
Resource localResource = local.findByRemoteName(remoteResource.getName(), false);
|
||||||
|
if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
|
||||||
|
remotelyUpdated.add(remoteResource);
|
||||||
|
} catch(RecordNotFoundException e) {
|
||||||
|
remotelyAdded.add(remoteResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE 3: pull remote changes from server
|
||||||
|
syncResult.stats.numInserts = pullNew(local, remote, remotelyAdded);
|
||||||
|
syncResult.stats.numUpdates = pullChanged(local, remote, remotelyUpdated);
|
||||||
|
|
||||||
|
Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore");
|
||||||
|
local.deleteAllExceptRemoteNames(remoteResources);
|
||||||
|
local.commit();
|
||||||
|
|
||||||
|
// update collection CTag
|
||||||
|
Log.i(TAG, "Sync complete, fetching new CTag");
|
||||||
|
local.setCTag(remote.getCTag());
|
||||||
|
local.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private int pushDeleted(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
|
||||||
|
int count = 0;
|
||||||
Resource[] deletedResources = local.findDeleted();
|
Resource[] deletedResources = local.findDeleted();
|
||||||
if (deletedResources != null) {
|
if (deletedResources != null) {
|
||||||
Log.i(TAG, "Remotely removing " + deletedResources.length + " deleted resource(s) (if not changed)");
|
Log.i(TAG, "Remotely removing " + deletedResources.length + " deleted resource(s) (if not changed)");
|
||||||
for (Resource res : deletedResources) {
|
for (Resource res : deletedResources) {
|
||||||
try {
|
try {
|
||||||
if (res.getName() != null) // is this resource even present remotely?
|
if (res.getName() != null) // is this resource even present remotely?
|
||||||
dav.delete(res);
|
remote.delete(res);
|
||||||
} catch(NotFoundException e) {
|
} catch(NotFoundException e) {
|
||||||
Log.i(TAG, "Locally-deleted resource has already been removed from server");
|
Log.i(TAG, "Locally-deleted resource has already been removed from server");
|
||||||
} catch(PreconditionFailedException e) {
|
} catch(PreconditionFailedException e) {
|
||||||
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
|
Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile");
|
||||||
}
|
}
|
||||||
fetchCollection = true;
|
|
||||||
local.delete(res);
|
local.delete(res);
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
local.commit();
|
local.commit();
|
||||||
}
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
// upload new resources
|
private int pushNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
|
||||||
|
int count = 0;
|
||||||
Resource[] newResources = local.findNew();
|
Resource[] newResources = local.findNew();
|
||||||
if (newResources != null) {
|
if (newResources != null) {
|
||||||
Log.i(TAG, "Uploading " + newResources.length + " new resource(s) (if not existing)");
|
Log.i(TAG, "Uploading " + newResources.length + " new resource(s) (if not existing)");
|
||||||
for (Resource res : newResources) {
|
for (Resource res : newResources) {
|
||||||
try {
|
try {
|
||||||
dav.add(res);
|
remote.add(res);
|
||||||
} catch(PreconditionFailedException e) {
|
} catch(PreconditionFailedException e) {
|
||||||
Log.i(TAG, "Didn't overwrite existing resource with other content");
|
Log.i(TAG, "Didn't overwrite existing resource with other content");
|
||||||
} catch (ValidationException e) {
|
} catch (ValidationException e) {
|
||||||
Log.e(TAG, "Couldn't create entity for adding: " + e.toString());
|
Log.e(TAG, "Couldn't create entity for adding: " + e.toString());
|
||||||
}
|
}
|
||||||
fetchCollection = true;
|
|
||||||
local.clearDirty(res);
|
local.clearDirty(res);
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
local.commit();
|
local.commit();
|
||||||
}
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
// upload modified resources
|
private int pushDirty(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote) throws LocalStorageException, IOException, HttpException {
|
||||||
|
int count = 0;
|
||||||
Resource[] dirtyResources = local.findDirty();
|
Resource[] dirtyResources = local.findDirty();
|
||||||
if (dirtyResources != null) {
|
if (dirtyResources != null) {
|
||||||
Log.i(TAG, "Uploading " + dirtyResources.length + " modified resource(s) (if not changed)");
|
Log.i(TAG, "Uploading " + dirtyResources.length + " modified resource(s) (if not changed)");
|
||||||
for (Resource res : dirtyResources) {
|
for (Resource res : dirtyResources) {
|
||||||
try {
|
try {
|
||||||
dav.update(res);
|
remote.update(res);
|
||||||
} catch(PreconditionFailedException e) {
|
} catch(PreconditionFailedException e) {
|
||||||
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile");
|
Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile");
|
||||||
} catch (ValidationException e) {
|
} catch (ValidationException e) {
|
||||||
Log.e(TAG, "Couldn't create entity for updating: " + e.toString());
|
Log.e(TAG, "Couldn't create entity for updating: " + e.toString());
|
||||||
}
|
}
|
||||||
fetchCollection = true;
|
|
||||||
local.clearDirty(res);
|
local.clearDirty(res);
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
local.commit();
|
local.commit();
|
||||||
}
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pullNew(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToAdd) throws LocalStorageException, IOException, HttpException {
|
||||||
// PHASE 2A: FETCH REMOTE COLLECTION STATUS
|
int count = 0;
|
||||||
// has collection changed -> fetch resources?
|
Log.i(TAG, "Fetching " + resourcesToAdd.size() + " new remote resource(s)");
|
||||||
if (manualSync) {
|
|
||||||
Log.i(TAG, "Synchronization forced");
|
|
||||||
fetchCollection = true;
|
|
||||||
}
|
|
||||||
if (!fetchCollection) {
|
|
||||||
String currentCTag = dav.getCTag(),
|
|
||||||
lastCTag = local.getCTag();
|
|
||||||
if (currentCTag == null || !currentCTag.equals(lastCTag))
|
|
||||||
fetchCollection = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fetchCollection)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// PHASE 2B: FETCH REMOTE COLLECTION SUMMARY
|
|
||||||
// fetch remote resources -> add/overwrite local resources
|
|
||||||
Log.i(TAG, "Fetching remote resource list");
|
|
||||||
|
|
||||||
Set<Resource> resourcesToAdd = new HashSet<Resource>(),
|
|
||||||
resourcesToUpdate = new HashSet<Resource>();
|
|
||||||
|
|
||||||
Resource[] remoteResources = dav.getMemberETags();
|
|
||||||
if (remoteResources == null) // failure
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (Resource remoteResource : remoteResources) {
|
|
||||||
Resource localResource = local.findByRemoteName(remoteResource.getName(), true);
|
|
||||||
if (localResource == null)
|
|
||||||
resourcesToAdd.add(remoteResource);
|
|
||||||
else if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag()))
|
|
||||||
resourcesToUpdate.add(remoteResource);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHASE 3: DOWNLOAD NEW/REMOTELY-CHANGED RESOURCES
|
|
||||||
Log.i(TAG, "Adding " + resourcesToAdd.size() + " remote resource(s)");
|
|
||||||
if (!resourcesToAdd.isEmpty()) {
|
if (!resourcesToAdd.isEmpty()) {
|
||||||
for (Resource res : dav.multiGet(resourcesToAdd.toArray(new Resource[0]))) {
|
for (Resource res : remote.multiGet(resourcesToAdd.toArray(new Resource[0]))) {
|
||||||
Log.i(TAG, "Adding " + res.getName());
|
Log.i(TAG, "Adding " + res.getName());
|
||||||
local.add(res);
|
local.add(res);
|
||||||
|
local.commit();
|
||||||
if (++syncResult.stats.numInserts % MAX_UPDATES_BEFORE_COMMIT == 0) // avoid TransactionTooLargeException
|
count++;
|
||||||
local.commit();
|
|
||||||
}
|
}
|
||||||
local.commit();
|
|
||||||
}
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(TAG, "Updating from " + resourcesToUpdate.size() + " remote resource(s)");
|
private int pullChanged(LocalCollection<? extends Resource> local, RemoteCollection<? extends Resource> remote, Set<Resource> resourcesToUpdate) throws LocalStorageException, IOException, HttpException {
|
||||||
|
int count = 0;
|
||||||
|
Log.i(TAG, "Fetching " + resourcesToUpdate.size() + " updated remote resource(s)");
|
||||||
if (!resourcesToUpdate.isEmpty())
|
if (!resourcesToUpdate.isEmpty())
|
||||||
for (Resource res : dav.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
|
for (Resource res : remote.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
|
||||||
Log.i(TAG, "Updating " + res.getName());
|
Log.i(TAG, "Updating " + res.getName());
|
||||||
local.updateByRemoteName(res);
|
local.updateByRemoteName(res);
|
||||||
|
local.commit();
|
||||||
if (++syncResult.stats.numUpdates % MAX_UPDATES_BEFORE_COMMIT == 0) // avoid TransactionTooLargeException
|
count++;
|
||||||
local.commit();
|
|
||||||
}
|
}
|
||||||
local.commit();
|
return count;
|
||||||
|
|
||||||
// delete remotely removed resources
|
|
||||||
Log.i(TAG, "Removing resources that are missing remotely");
|
|
||||||
local.deleteAllExceptRemoteNames(remoteResources);
|
|
||||||
local.commit();
|
|
||||||
|
|
||||||
// update collection CTag
|
|
||||||
local.setCTag(dav.getCTag());
|
|
||||||
local.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user