diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java index e917004f..64c04e38 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java @@ -191,7 +191,7 @@ public class LocalAddressBook extends LocalCollection { try { @Cleanup EntityIterator iter = ContactsContract.RawContacts.newEntityIterator(providerClient.query( - ContactsContract.RawContactsEntity.CONTENT_URI, + syncAdapterURI(ContactsContract.RawContactsEntity.CONTENT_URI), null, RawContacts._ID + "=" + c.getLocalID(), null, null)); @@ -252,8 +252,6 @@ public class LocalAddressBook extends LocalCollection { break; } } - - Log.i(TAG, "Populated contact: " + c); } else throw new RecordNotFoundException(); } catch(RemoteException ex) { @@ -341,7 +339,7 @@ public class LocalAddressBook extends LocalCollection { break; case Phone.TYPE_CUSTOM: String customType = row.getAsString(Phone.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) number.addType(TelephoneType.get(labelToXName(customType))); } if (row.getAsBoolean(Phone.IS_PRIMARY)) @@ -363,7 +361,7 @@ public class LocalAddressBook extends LocalCollection { break; case Email.TYPE_CUSTOM: String customType = row.getAsString(Email.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) email.addType(EmailType.get(labelToXName(customType))); } if (row.getAsBoolean(Email.IS_PRIMARY)) @@ -393,18 +391,18 @@ public class LocalAddressBook extends LocalCollection { title = row.getAsString(Organization.TITLE), role = row.getAsString(Organization.JOB_DESCRIPTION); - if (!StringUtils.isEmpty(company) || !StringUtils.isEmpty(department)) { + if (StringUtils.isNotEmpty(company) || StringUtils.isNotEmpty(department)) { ezvcard.property.Organization org = new ezvcard.property.Organization(); - if (!StringUtils.isEmpty(company)) + if (StringUtils.isNotEmpty(company)) org.addValue(company); - if (!StringUtils.isEmpty(department)) + if (StringUtils.isNotEmpty(department)) org.addValue(department); c.setOrganization(org); } - if (!StringUtils.isEmpty(title)) + if (StringUtils.isNotEmpty(title)) c.setJobTitle(title); - if (!StringUtils.isEmpty(role)) + if (StringUtils.isNotEmpty(role)) c.setJobDescription(role); } @@ -454,7 +452,7 @@ public class LocalAddressBook extends LocalCollection { break; case Im.TYPE_CUSTOM: String customType = row.getAsString(Im.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) impp.addType(ImppType.get(labelToXName(customType))); } @@ -483,7 +481,7 @@ public class LocalAddressBook extends LocalCollection { break; case StructuredPostal.TYPE_CUSTOM: String customType = row.getAsString(StructuredPostal.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) address.addType(AddressType.get(labelToXName(customType))); break; } @@ -626,7 +624,7 @@ public class LocalAddressBook extends LocalCollection { types.add(RelatedType.SPOUSE); case Relation.TYPE_CUSTOM: String customType = row.getAsString(Relation.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) types.add(RelatedType.get(customType)); } } @@ -643,7 +641,7 @@ public class LocalAddressBook extends LocalCollection { break; case SipAddress.TYPE_CUSTOM: String customType = row.getAsString(SipAddress.LABEL); - if (!StringUtils.isEmpty(customType)) + if (StringUtils.isNotEmpty(customType)) impp.addType(ImppType.get(labelToXName(customType))); } c.getImpps().add(impp); diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java index 14b5f193..abbac808 100644 --- a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java @@ -16,6 +16,8 @@ import android.content.ContentProviderOperation.Builder; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; +import android.content.Entity; +import android.content.EntityIterator; import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; @@ -260,7 +262,7 @@ public class LocalCalendar extends LocalCollection { " AND " + Events.ORIGINAL_SYNC_ID + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name pendingOperations.add(ContentProviderOperation .newDelete(entriesURI()) - .withSelection(where, new String[] { String.valueOf(id) }) + .withSelection(where, new String[]{String.valueOf(id)}) .withYieldAllowed(true) .build() ); @@ -273,7 +275,7 @@ public class LocalCalendar extends LocalCollection { // delete all exceptions of this event, too pendingOperations.add(ContentProviderOperation .newDelete(entriesURI()) - .withSelection(Events.ORIGINAL_ID+"=?", new String[] { Long.toString(resource.getLocalID()) }) + .withSelection(Events.ORIGINAL_ID + "=?", new String[] { Long.toString(resource.getLocalID()) }) .build() ); } @@ -286,7 +288,7 @@ public class LocalCalendar extends LocalCollection { pendingOperations.add(ContentProviderOperation .newUpdate(entriesURI()) .withValue(Events.DIRTY, 0) - .withSelection(Events.ORIGINAL_ID+"=?", new String[] { Long.toString(resource.getLocalID()) }) + .withSelection(Events.ORIGINAL_ID + "=?", new String[] { Long.toString(resource.getLocalID()) }) .build() ); } @@ -296,102 +298,119 @@ public class LocalCalendar extends LocalCollection { @Override public void populate(Resource resource) throws LocalStorageException { - Event e = (Event)resource; - + Event event = (Event)resource; + try { - @Cleanup 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, - /* 20 */ Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_INSTANCE_TIME - }, 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)); - - boolean allDay = cursor.getInt(7) != 0; - long tsStart = cursor.getLong(3), - tsEnd = cursor.getLong(4); - String duration = cursor.getString(18); - - String tzId = null; - if (allDay) { - e.setDtStart(tsStart, null); - // provide only DTEND and not DURATION for all-day events - if (tsEnd == 0) { - Dur dur = new Dur(duration); - java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart)); - tsEnd = dEnd.getTime(); - } - e.setDtEnd(tsEnd, null); - - } else { - // use the start time zone for the end time, too - // because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only - tzId = cursor.getString(5); - e.setDtStart(tsStart, tzId); - if (tsEnd != 0) - e.setDtEnd(tsEnd, tzId); - else if (!StringUtils.isEmpty(duration)) - e.setDuration(new Duration(new Dur(duration))); - } - - // recurrence - try { - String strRRule = cursor.getString(10); - if (!StringUtils.isEmpty(strRRule)) - e.setRrule(new RRule(strRRule)); - - String strRDate = cursor.getString(11); - if (!StringUtils.isEmpty(strRDate)) { - RDate rDate = new RDate(); - rDate.setValue(strRDate); - e.setRdate(rDate); - } - - String strExRule = cursor.getString(12); - if (!StringUtils.isEmpty(strExRule)) { - ExRule exRule = new ExRule(); - exRule.setValue(strExRule); - e.setExrule(exRule); - } - - String strExDate = cursor.getString(13); - if (!StringUtils.isEmpty(strExDate)) { - // 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); + @Cleanup EntityIterator iterEvents = CalendarContract.EventsEntity.newEntityIterator( + providerClient.query( + syncAdapterURI(CalendarContract.EventsEntity.CONTENT_URI), + null, Events._ID + "=" + event.getLocalID(), + null, null), + providerClient + ); + while (iterEvents.hasNext()) { + Entity e = iterEvents.next(); + + ContentValues values = e.getEntityValues(); + populateEvent(event, values); + + List subValues = e.getSubValues(); + for (Entity.NamedContentValues subValue : subValues) { + values = subValue.values; + if (Attendees.CONTENT_URI.equals(subValue.uri)) + populateAttendee(event, values); + if (Reminders.CONTENT_URI.equals(subValue.uri)) + populateReminder(event, values); } - // recurrence exceptions - if (!cursor.isNull(21)) { - long originalInstanceTime = cursor.getLong(21); - boolean originalAllDay = cursor.getInt(20) != 0; - Date originalDate = originalAllDay ? - new Date(originalInstanceTime) : - new DateTime(originalInstanceTime); - if (originalDate instanceof DateTime) - ((DateTime)originalDate).setUtc(true); - e.setRecurrenceId(new RecurrenceId(originalDate)); - } else - // this event may have exceptions - populateExceptions(e); - - // status - switch (cursor.getInt(8)) { + populateExceptions(event); + } + } catch (RemoteException ex) { + throw new LocalStorageException("Couldn't process locally stored event", ex); + } + } + + protected void populateEvent(Event e, ContentValues values) { + e.setUid(values.getAsString(entryColumnUID())); + + e.setSummary(values.getAsString(Events.TITLE)); + e.setLocation(values.getAsString(Events.EVENT_LOCATION)); + e.setDescription(values.getAsString(Events.DESCRIPTION)); + + boolean allDay = values.getAsBoolean(Events.ALL_DAY); + long tsStart = values.getAsLong(Events.DTSTART); + Long tsEnd = values.getAsLong(Events.DTEND); + String duration = values.getAsString(Events.DURATION); + + String tzId = null; + if (allDay) { + e.setDtStart(tsStart, null); + if (tsEnd == null) { + Dur dur = new Dur(duration); + java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart)); + tsEnd = dEnd.getTime(); + } + e.setDtEnd(tsEnd, null); + + } else { + // use the start time zone for the end time, too + // because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only + tzId = values.getAsString(Events.EVENT_TIMEZONE); + e.setDtStart(tsStart, tzId); + if (tsEnd != null) + e.setDtEnd(tsEnd, tzId); + else if (!StringUtils.isEmpty(duration)) + e.setDuration(new Duration(new Dur(duration))); + } + + // recurrence + try { + String strRRule = values.getAsString(Events.RRULE); + if (!StringUtils.isEmpty(strRRule)) + e.setRrule(new RRule(strRRule)); + + String strRDate = values.getAsString(Events.RDATE); + if (!StringUtils.isEmpty(strRDate)) { + RDate rDate = new RDate(); + rDate.setValue(strRDate); + e.setRdate(rDate); + } + + String strExRule = values.getAsString(Events.EXRULE); + if (!StringUtils.isEmpty(strExRule)) { + ExRule exRule = new ExRule(); + exRule.setValue(strExRule); + e.setExrule(exRule); + } + + String strExDate = values.getAsString(Events.EXDATE); + if (!StringUtils.isEmpty(strExDate)) { + // 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); + } + + if (values.containsKey(Events.ORIGINAL_INSTANCE_TIME)) { + // this event is an exception of a recurring event + long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME); + boolean originalAllDay = values.getAsBoolean(Events.ORIGINAL_ALL_DAY); + Date originalDate = originalAllDay ? + new Date(originalInstanceTime) : + new DateTime(originalInstanceTime); + if (originalDate instanceof DateTime) + ((DateTime)originalDate).setUtc(true); + e.setRecurrenceId(new RecurrenceId(originalDate)); + } + + // status + if (values.containsKey(Events.STATUS)) + switch (values.getAsInteger(Events.STATUS)) { case Events.STATUS_CONFIRMED: e.setStatus(Status.VEVENT_CONFIRMED); break; @@ -400,50 +419,37 @@ public class LocalCalendar extends LocalCollection { 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); + } + + // availability + e.setOpaque(values.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE); + + // set ORGANIZER only when there are attendees + if (values.getAsBoolean(Events.HAS_ATTENDEE_DATA) && values.containsKey(Events.ORGANIZER)) + try { + e.setOrganizer(new Organizer(new URI("mailto", values.getAsString(Events.ORGANIZER), null))); + } catch (URISyntaxException ex) { + Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex); + } + + // classification + switch (values.getAsInteger(Events.ACCESS_LEVEL)) { + case Events.ACCESS_CONFIDENTIAL: + case Events.ACCESS_PRIVATE: + e.setForPublic(false); + break; + case Events.ACCESS_PUBLIC: + e.setForPublic(true); } } void populateExceptions(Event e) throws RemoteException { - Uri exceptionsUri = Events.CONTENT_URI.buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .build(); - @Cleanup Cursor c = providerClient.query(exceptionsUri, new String[] { - /* 0 */ Events._ID, entryColumnRemoteName() - }, Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + @Cleanup Cursor c = providerClient.query(syncAdapterURI(Events.CONTENT_URI), + new String[]{Events._ID, entryColumnRemoteName()}, + Events.ORIGINAL_ID + "=?", new String[]{ String.valueOf(e.getLocalID()) }, null); while (c != null && c.moveToNext()) { long exceptionId = c.getLong(0); String exceptionRemoteName = c.getString(1); - Log.i(TAG, "Found exception ID " + exceptionId + " of original ID " + e.getLocalID()); try { Event exception = new Event(exceptionId, exceptionRemoteName, null); populate(exception); @@ -454,30 +460,22 @@ public class LocalCalendar extends LocalCollection { } } - void populateAttendees(Event e) throws RemoteException { - Uri attendeesUri = Attendees.CONTENT_URI.buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .build(); - @Cleanup 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) { + void populateAttendee(Event event, ContentValues values) throws RemoteException { + try { + Attendee attendee = new Attendee(new URI("mailto", values.getAsString(Attendees.ATTENDEE_EMAIL), null)); + ParameterList params = attendee.getParameters(); + + String cn = values.getAsString(Attendees.ATTENDEE_NAME); + if (cn != null) + params.add(new Cn(cn)); + + // type + int type = values.getAsInteger(Attendees.ATTENDEE_TYPE); + params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL); + + // role + int relationship = values.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP); + switch (relationship) { case Attendees.RELATIONSHIP_ORGANIZER: params.add(Role.CHAIR); break; @@ -488,10 +486,10 @@ public class LocalCalendar extends LocalCollection { break; case Attendees.RELATIONSHIP_NONE: params.add(Role.NON_PARTICIPANT); - } - - // status - switch (c.getInt(4)) { + } + + // status + switch (values.getAsInteger(Attendees.ATTENDEE_STATUS)) { case Attendees.ATTENDEE_STATUS_INVITED: params.add(PartStat.NEEDS_ACTION); break; @@ -504,37 +502,21 @@ public class LocalCalendar extends LocalCollection { case Attendees.ATTENDEE_STATUS_TENTATIVE: params.add(PartStat.TENTATIVE); break; - } - - e.getAttendees().add(attendee); - } catch (URISyntaxException ex) { - Log.e(TAG, "Couldn't parse attendee information, ignoring", ex); } + + event.getAttendees().add(attendee); + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't parse attendee information, ignoring", ex); } } - void populateReminders(Event e) throws RemoteException { - // reminders - Uri remindersUri = Reminders.CONTENT_URI.buildUpon() - .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") - .build(); - @Cleanup 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.getAlarms().add(alarm); - } + void populateReminder(Event event, ContentValues row) throws RemoteException { + VAlarm alarm = new VAlarm(new Dur(0, 0, row.getAsInteger(Reminders.MINUTES), 0)); + + PropertyList props = alarm.getProperties(); + props.add(Action.DISPLAY); + props.add(new Description(event.getSummary())); + event.getAlarms().add(alarm); } @@ -549,6 +531,7 @@ public class LocalCalendar extends LocalCollection { .withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0) .withValue(Events.DTSTART, event.getDtStartInMillis()) .withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID()) + .withValue(Events.HAS_ALARM, event.getAlarms().isEmpty() ? 0 : 1) .withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1) .withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1) .withValue(Events.GUESTS_CAN_MODIFY, 1) @@ -650,17 +633,14 @@ public class LocalCalendar extends LocalCollection { protected void removeDataRows(Resource resource) { Event event = (Event)resource; // delete exceptions - pendingOperations.add(ContentProviderOperation.newDelete(entriesURI()) - .withSelection(Events.ORIGINAL_ID + "=?", - new String[] { String.valueOf(event.getLocalID()) }).build()); + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Events.CONTENT_URI)) + .withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(event.getLocalID())}).build()); // delete attendees pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI)) - .withSelection(Attendees.EVENT_ID + "=?", - new String[] { String.valueOf(event.getLocalID()) }).build()); + .withSelection(Attendees.EVENT_ID + "=?", new String[] { String.valueOf(event.getLocalID()) }).build()); // delete reminders pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI)) - .withSelection(Reminders.EVENT_ID + "=?", - new String[] { String.valueOf(event.getLocalID()) }).build()); + .withSelection(Reminders.EVENT_ID + "=?", new String[] { String.valueOf(event.getLocalID()) }).build()); }