|
|
|
@ -1,786 +0,0 @@
|
|
|
|
|
/*
|
|
|
|
|
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
|
|
|
* All rights reserved. This program and the accompanying materials
|
|
|
|
|
* are made available under the terms of the GNU Public License v3.0
|
|
|
|
|
* which accompanies this distribution, and is available at
|
|
|
|
|
* http://www.gnu.org/licenses/gpl.html
|
|
|
|
|
*/
|
|
|
|
|
package at.bitfire.davdroid.resource;
|
|
|
|
|
|
|
|
|
|
import android.accounts.Account;
|
|
|
|
|
import android.annotation.SuppressLint;
|
|
|
|
|
import android.annotation.TargetApi;
|
|
|
|
|
import android.content.ContentProviderClient;
|
|
|
|
|
import android.content.ContentProviderOperation;
|
|
|
|
|
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;
|
|
|
|
|
import android.os.Build;
|
|
|
|
|
import android.os.RemoteException;
|
|
|
|
|
import android.provider.CalendarContract;
|
|
|
|
|
import android.provider.CalendarContract.Attendees;
|
|
|
|
|
import android.provider.CalendarContract.Calendars;
|
|
|
|
|
import android.provider.CalendarContract.Events;
|
|
|
|
|
import android.provider.CalendarContract.Reminders;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
|
|
import net.fortuna.ical4j.model.Date;
|
|
|
|
|
import net.fortuna.ical4j.model.DateTime;
|
|
|
|
|
import net.fortuna.ical4j.model.Dur;
|
|
|
|
|
import net.fortuna.ical4j.model.Parameter;
|
|
|
|
|
import net.fortuna.ical4j.model.ParameterList;
|
|
|
|
|
import net.fortuna.ical4j.model.PropertyList;
|
|
|
|
|
import net.fortuna.ical4j.model.component.VAlarm;
|
|
|
|
|
import net.fortuna.ical4j.model.parameter.Cn;
|
|
|
|
|
import net.fortuna.ical4j.model.parameter.CuType;
|
|
|
|
|
import net.fortuna.ical4j.model.parameter.PartStat;
|
|
|
|
|
import net.fortuna.ical4j.model.parameter.Role;
|
|
|
|
|
import net.fortuna.ical4j.model.parameter.Rsvp;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Action;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Attendee;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Description;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Duration;
|
|
|
|
|
import net.fortuna.ical4j.model.property.ExDate;
|
|
|
|
|
import net.fortuna.ical4j.model.property.ExRule;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Organizer;
|
|
|
|
|
import net.fortuna.ical4j.model.property.RDate;
|
|
|
|
|
import net.fortuna.ical4j.model.property.RRule;
|
|
|
|
|
import net.fortuna.ical4j.model.property.RecurrenceId;
|
|
|
|
|
import net.fortuna.ical4j.model.property.Status;
|
|
|
|
|
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
import java.net.URISyntaxException;
|
|
|
|
|
import java.text.DateFormat;
|
|
|
|
|
import java.text.ParseException;
|
|
|
|
|
import java.text.SimpleDateFormat;
|
|
|
|
|
import java.util.LinkedList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
import at.bitfire.davdroid.DavUtils;
|
|
|
|
|
import at.bitfire.davdroid.DateUtils;
|
|
|
|
|
import at.bitfire.davdroid.webdav.WebDavResource;
|
|
|
|
|
import lombok.Cleanup;
|
|
|
|
|
import lombok.Getter;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Represents a locally stored calendar, containing Events.
|
|
|
|
|
* Communicates with the Android Contacts Provider which uses an SQLite
|
|
|
|
|
* database to store the contacts.
|
|
|
|
|
*/
|
|
|
|
|
public class LocalCalendar extends LocalCollection<Event> {
|
|
|
|
|
private static final String TAG = "davdroid.LocalCalendar";
|
|
|
|
|
|
|
|
|
|
@Getter protected String url;
|
|
|
|
|
@Getter protected long id;
|
|
|
|
|
|
|
|
|
|
protected static final String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;
|
|
|
|
|
|
|
|
|
|
/* database fields */
|
|
|
|
|
|
|
|
|
|
@Override protected Uri entriesURI() { return syncAdapterURI(Events.CONTENT_URI); }
|
|
|
|
|
@Override protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; }
|
|
|
|
|
@Override protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; }
|
|
|
|
|
@Override protected String entryColumnParentID() { return Events.CALENDAR_ID; }
|
|
|
|
|
@Override protected String entryColumnID() { return Events._ID; }
|
|
|
|
|
@Override protected String entryColumnRemoteName() { return Events._SYNC_ID; }
|
|
|
|
|
@Override protected String entryColumnETag() { return Events.SYNC_DATA1; }
|
|
|
|
|
@Override protected String entryColumnDirty() { return Events.DIRTY; }
|
|
|
|
|
@Override protected String entryColumnDeleted() { return Events.DELETED; }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
|
|
|
protected String entryColumnUID() {
|
|
|
|
|
return (android.os.Build.VERSION.SDK_INT >= 17) ?
|
|
|
|
|
Events.UID_2445 : Events.SYNC_DATA2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* class methods, constructor */
|
|
|
|
|
|
|
|
|
|
@SuppressLint("InlinedApi")
|
|
|
|
|
public static Uri create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException {
|
|
|
|
|
@Cleanup("release") final ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
|
|
|
|
|
if (client == null)
|
|
|
|
|
throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)");
|
|
|
|
|
|
|
|
|
|
ContentValues values = new ContentValues();
|
|
|
|
|
values.put(Calendars.ACCOUNT_NAME, account.name);
|
|
|
|
|
values.put(Calendars.ACCOUNT_TYPE, account.type);
|
|
|
|
|
values.put(Calendars.NAME, info.getURL());
|
|
|
|
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle());
|
|
|
|
|
values.put(Calendars.CALENDAR_COLOR, info.getColor() != null ? info.getColor() : DavUtils.calendarGreen);
|
|
|
|
|
values.put(Calendars.OWNER_ACCOUNT, account.name);
|
|
|
|
|
values.put(Calendars.SYNC_EVENTS, 1);
|
|
|
|
|
values.put(Calendars.VISIBLE, 1);
|
|
|
|
|
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT);
|
|
|
|
|
|
|
|
|
|
if (info.isReadOnly())
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ);
|
|
|
|
|
else {
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
|
|
|
|
|
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1);
|
|
|
|
|
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (android.os.Build.VERSION.SDK_INT >= 15) {
|
|
|
|
|
values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE);
|
|
|
|
|
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.getTimezone() != null)
|
|
|
|
|
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(info.getTimezone()));
|
|
|
|
|
|
|
|
|
|
Log.i(TAG, "Inserting calendar: " + values.toString());
|
|
|
|
|
try {
|
|
|
|
|
return client.insert(calendarsURI(account), values);
|
|
|
|
|
} catch (RemoteException e) {
|
|
|
|
|
throw new LocalStorageException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException {
|
|
|
|
|
@Cleanup Cursor cursor = providerClient.query(calendarsURI(account),
|
|
|
|
|
new String[] { Calendars._ID, Calendars.NAME },
|
|
|
|
|
Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null);
|
|
|
|
|
|
|
|
|
|
LinkedList<LocalCalendar> calendars = new LinkedList<>();
|
|
|
|
|
while (cursor != null && cursor.moveToNext())
|
|
|
|
|
calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1)));
|
|
|
|
|
return calendars.toArray(new LocalCalendar[calendars.size()]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) {
|
|
|
|
|
super(account, providerClient);
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.url = url;
|
|
|
|
|
sqlFilter = "ORIGINAL_ID IS NULL";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* collection operations */
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String getCTag() throws LocalStorageException {
|
|
|
|
|
try {
|
|
|
|
|
@Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id),
|
|
|
|
|
new String[] { COLLECTION_COLUMN_CTAG }, null, null, null);
|
|
|
|
|
if (c != null && c.moveToFirst())
|
|
|
|
|
return c.getString(0);
|
|
|
|
|
else
|
|
|
|
|
throw new LocalStorageException("Couldn't query calendar CTag");
|
|
|
|
|
} catch(RemoteException e) {
|
|
|
|
|
throw new LocalStorageException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setCTag(String cTag) throws LocalStorageException {
|
|
|
|
|
ContentValues values = new ContentValues(1);
|
|
|
|
|
values.put(COLLECTION_COLUMN_CTAG, cTag);
|
|
|
|
|
try {
|
|
|
|
|
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
|
|
|
|
} catch(RemoteException e) {
|
|
|
|
|
throw new LocalStorageException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void updateMetaData(WebDavResource.Properties properties) throws LocalStorageException {
|
|
|
|
|
ContentValues values = new ContentValues();
|
|
|
|
|
|
|
|
|
|
final String displayName = properties.getDisplayName();
|
|
|
|
|
if (displayName != null)
|
|
|
|
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, displayName);
|
|
|
|
|
|
|
|
|
|
final Integer color = properties.getColor();
|
|
|
|
|
if (color != null)
|
|
|
|
|
values.put(Calendars.CALENDAR_COLOR, color);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (values.size() > 0)
|
|
|
|
|
providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null);
|
|
|
|
|
} catch(RemoteException e) {
|
|
|
|
|
throw new LocalStorageException(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public long[] findUpdated() throws LocalStorageException {
|
|
|
|
|
// mark (recurring) events with changed/deleted exceptions as dirty
|
|
|
|
|
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
|
|
|
|
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
|
|
|
|
ContentValues dirty = new ContentValues(1);
|
|
|
|
|
dirty.put(CalendarContract.Events.DIRTY, 1);
|
|
|
|
|
try {
|
|
|
|
|
int rows = providerClient.update(entriesURI(), dirty, where, null);
|
|
|
|
|
if (rows > 0)
|
|
|
|
|
Log.d(TAG, rows + " event(s) marked as dirty because of dirty/deleted exceptions");
|
|
|
|
|
} catch (RemoteException e) {
|
|
|
|
|
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// new find and return updated (master) events
|
|
|
|
|
return super.findUpdated();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* create/update/delete */
|
|
|
|
|
|
|
|
|
|
public Event newResource(long localID, String resourceName, String eTag) {
|
|
|
|
|
return new Event(localID, resourceName, eTag);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int deleteAllExceptRemoteNames(Resource[] remoteResources) throws LocalStorageException {
|
|
|
|
|
List<String> sqlFileNames = new LinkedList<>();
|
|
|
|
|
for (Resource res : remoteResources)
|
|
|
|
|
sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName()));
|
|
|
|
|
|
|
|
|
|
// delete master events
|
|
|
|
|
String where = entryColumnParentID() + "=?";
|
|
|
|
|
where += sqlFileNames.isEmpty() ?
|
|
|
|
|
" AND " + entryColumnRemoteName() + " IS NOT NULL" : // don't retain anything (delete all)
|
|
|
|
|
" AND " + entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; // retain by remote file name
|
|
|
|
|
if (sqlFilter != null)
|
|
|
|
|
where += " AND (" + sqlFilter + ")";
|
|
|
|
|
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
|
|
|
|
.withSelection(where, new String[] { String.valueOf(id) })
|
|
|
|
|
.build());
|
|
|
|
|
|
|
|
|
|
// delete exceptions, too
|
|
|
|
|
where = entryColumnParentID() + "=?";
|
|
|
|
|
where += sqlFileNames.isEmpty() ?
|
|
|
|
|
" AND " + Events.ORIGINAL_SYNC_ID + " IS NOT NULL" : // don't retain anything (delete all)
|
|
|
|
|
" 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)})
|
|
|
|
|
.withYieldAllowed(true)
|
|
|
|
|
.build()
|
|
|
|
|
);
|
|
|
|
|
return commit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void delete(Resource resource) {
|
|
|
|
|
super.delete(resource);
|
|
|
|
|
|
|
|
|
|
// delete all exceptions of this event, too
|
|
|
|
|
pendingOperations.add(ContentProviderOperation
|
|
|
|
|
.newDelete(entriesURI())
|
|
|
|
|
.withSelection(Events.ORIGINAL_ID + "=?", new String[] { String.valueOf(resource.getLocalID()) })
|
|
|
|
|
.build()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void clearDirty(Resource resource) {
|
|
|
|
|
super.clearDirty(resource);
|
|
|
|
|
|
|
|
|
|
// clear dirty flag of all exceptions of this event
|
|
|
|
|
pendingOperations.add(ContentProviderOperation
|
|
|
|
|
.newUpdate(entriesURI())
|
|
|
|
|
.withValue(Events.DIRTY, 0)
|
|
|
|
|
.withSelection(Events.ORIGINAL_ID + "=?", new String[]{String.valueOf(resource.getLocalID())})
|
|
|
|
|
.build()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* methods for populating the data object from the content provider */
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void populate(Resource resource) throws LocalStorageException {
|
|
|
|
|
Event event = (Event)resource;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
@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<Entity.NamedContentValues> 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.summary = values.getAsString(Events.TITLE);
|
|
|
|
|
e.location = values.getAsString(Events.EVENT_LOCATION);
|
|
|
|
|
e.description = values.getAsString(Events.DESCRIPTION);
|
|
|
|
|
|
|
|
|
|
final boolean allDay = values.getAsInteger(Events.ALL_DAY) != 0;
|
|
|
|
|
final long tsStart = values.getAsLong(Events.DTSTART);
|
|
|
|
|
final String duration = values.getAsString(Events.DURATION);
|
|
|
|
|
|
|
|
|
|
String tzId;
|
|
|
|
|
Long tsEnd = values.getAsLong(Events.DTEND);
|
|
|
|
|
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.duration = new Duration(new Dur(duration));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// recurrence
|
|
|
|
|
try {
|
|
|
|
|
String strRRule = values.getAsString(Events.RRULE);
|
|
|
|
|
if (!StringUtils.isEmpty(strRRule))
|
|
|
|
|
e.rrule = new RRule(strRRule);
|
|
|
|
|
|
|
|
|
|
String strRDate = values.getAsString(Events.RDATE);
|
|
|
|
|
if (!StringUtils.isEmpty(strRDate)) {
|
|
|
|
|
RDate rDate = (RDate)DateUtils.androidStringToRecurrenceSet(strRDate, RDate.class, allDay);
|
|
|
|
|
e.getRdates().add(rDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String strExRule = values.getAsString(Events.EXRULE);
|
|
|
|
|
if (!StringUtils.isEmpty(strExRule)) {
|
|
|
|
|
ExRule exRule = new ExRule();
|
|
|
|
|
exRule.setValue(strExRule);
|
|
|
|
|
e.exrule = exRule;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String strExDate = values.getAsString(Events.EXDATE);
|
|
|
|
|
if (!StringUtils.isEmpty(strExDate)) {
|
|
|
|
|
ExDate exDate = (ExDate)DateUtils.androidStringToRecurrenceSet(strExDate, ExDate.class, allDay);
|
|
|
|
|
e.getExdates().add(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 = false;
|
|
|
|
|
if (values.containsKey(Events.ORIGINAL_ALL_DAY))
|
|
|
|
|
originalAllDay = values.getAsInteger(Events.ORIGINAL_ALL_DAY) != 0;
|
|
|
|
|
|
|
|
|
|
Date originalDate = originalAllDay ?
|
|
|
|
|
new Date(originalInstanceTime) :
|
|
|
|
|
new DateTime(originalInstanceTime);
|
|
|
|
|
if (originalDate instanceof DateTime)
|
|
|
|
|
((DateTime)originalDate).setUtc(true);
|
|
|
|
|
e.recurrenceId = new RecurrenceId(originalDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// status
|
|
|
|
|
if (values.containsKey(Events.STATUS))
|
|
|
|
|
switch (values.getAsInteger(Events.STATUS)) {
|
|
|
|
|
case Events.STATUS_CONFIRMED:
|
|
|
|
|
e.status = Status.VEVENT_CONFIRMED;
|
|
|
|
|
break;
|
|
|
|
|
case Events.STATUS_TENTATIVE:
|
|
|
|
|
e.status = Status.VEVENT_TENTATIVE;
|
|
|
|
|
break;
|
|
|
|
|
case Events.STATUS_CANCELED:
|
|
|
|
|
e.status = Status.VEVENT_CANCELLED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// availability
|
|
|
|
|
e.opaque = values.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE;
|
|
|
|
|
|
|
|
|
|
// set ORGANIZER if there's attendee data
|
|
|
|
|
if (values.getAsInteger(Events.HAS_ATTENDEE_DATA) != 0)
|
|
|
|
|
try {
|
|
|
|
|
e.organizer = new Organizer(new URI("mailto", values.getAsString(Events.ORGANIZER), null));
|
|
|
|
|
} catch (URISyntaxException ex) {
|
|
|
|
|
Log.e(TAG, "Error when creating ORGANIZER mailto URI, ignoring", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// classification
|
|
|
|
|
switch (values.getAsInteger(Events.ACCESS_LEVEL)) {
|
|
|
|
|
case Events.ACCESS_CONFIDENTIAL:
|
|
|
|
|
case Events.ACCESS_PRIVATE:
|
|
|
|
|
e.forPublic = false;
|
|
|
|
|
break;
|
|
|
|
|
case Events.ACCESS_PUBLIC:
|
|
|
|
|
e.forPublic = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void populateExceptions(Event e) throws RemoteException {
|
|
|
|
|
@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);
|
|
|
|
|
try {
|
|
|
|
|
Event exception = new Event(exceptionId, exceptionRemoteName, null);
|
|
|
|
|
populate(exception);
|
|
|
|
|
e.getExceptions().add(exception);
|
|
|
|
|
} catch (LocalStorageException ex) {
|
|
|
|
|
Log.e(TAG, "Couldn't find exception details, ignoring");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void populateAttendee(Event event, ContentValues values) {
|
|
|
|
|
try {
|
|
|
|
|
final Attendee attendee;
|
|
|
|
|
final String
|
|
|
|
|
email = values.getAsString(Attendees.ATTENDEE_EMAIL),
|
|
|
|
|
idNS = values.getAsString(Attendees.ATTENDEE_ID_NAMESPACE),
|
|
|
|
|
id = values.getAsString(Attendees.ATTENDEE_IDENTITY);
|
|
|
|
|
if (idNS != null || id != null) {
|
|
|
|
|
// attendee identified by namespace and ID
|
|
|
|
|
attendee = new Attendee(new URI(idNS, id, null));
|
|
|
|
|
if (email != null)
|
|
|
|
|
attendee.getParameters().add(new iCalendar.Email(email));
|
|
|
|
|
} else
|
|
|
|
|
// attendee identified by email address
|
|
|
|
|
attendee = new Attendee(new URI("mailto", email, null));
|
|
|
|
|
final 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:
|
|
|
|
|
case Attendees.RELATIONSHIP_ATTENDEE:
|
|
|
|
|
case Attendees.RELATIONSHIP_PERFORMER:
|
|
|
|
|
case Attendees.RELATIONSHIP_SPEAKER:
|
|
|
|
|
params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT);
|
|
|
|
|
params.add(new Rsvp(true));
|
|
|
|
|
break;
|
|
|
|
|
case Attendees.RELATIONSHIP_NONE:
|
|
|
|
|
params.add(Role.NON_PARTICIPANT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// status
|
|
|
|
|
switch (values.getAsInteger(Attendees.ATTENDEE_STATUS)) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.getAttendees().add(attendee);
|
|
|
|
|
} catch (URISyntaxException ex) {
|
|
|
|
|
Log.e(TAG, "Couldn't parse attendee information, ignoring", ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void populateReminder(Event event, ContentValues row) {
|
|
|
|
|
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.summary));
|
|
|
|
|
event.getAlarms().add(alarm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* content builder methods */
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected Builder buildEntry(Builder builder, Resource resource, boolean update) {
|
|
|
|
|
final Event event = (Event)resource;
|
|
|
|
|
|
|
|
|
|
builder .withValue(Events.CALENDAR_ID, id)
|
|
|
|
|
.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)
|
|
|
|
|
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
|
|
|
|
|
|
|
|
|
if (event.recurrenceId == null) {
|
|
|
|
|
// this event is a "master event" (not an exception)
|
|
|
|
|
builder .withValue(entryColumnRemoteName(), event.getName())
|
|
|
|
|
.withValue(entryColumnETag(), event.getETag())
|
|
|
|
|
.withValue(entryColumnUID(), event.getUid());
|
|
|
|
|
} else {
|
|
|
|
|
// event is an exception
|
|
|
|
|
builder.withValue(Events.ORIGINAL_SYNC_ID, event.getName());
|
|
|
|
|
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY is set in buildExceptions.
|
|
|
|
|
// It's not possible to use only the RECURRENCE-ID to calculate
|
|
|
|
|
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY because iCloud sends DATE-TIME
|
|
|
|
|
// RECURRENCE-IDs even if the original event is an all-day event.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boolean recurring = false;
|
|
|
|
|
if (event.rrule != null) {
|
|
|
|
|
recurring = true;
|
|
|
|
|
builder.withValue(Events.RRULE, event.rrule.getValue());
|
|
|
|
|
}
|
|
|
|
|
if (!event.getRdates().isEmpty()) {
|
|
|
|
|
recurring = true;
|
|
|
|
|
try {
|
|
|
|
|
builder.withValue(Events.RDATE, DateUtils.recurrenceSetsToAndroidString(event.getRdates(), event.isAllDay()));
|
|
|
|
|
} catch (ParseException e) {
|
|
|
|
|
Log.e(TAG, "Couldn't parse RDate(s)", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (event.exrule != null)
|
|
|
|
|
builder.withValue(Events.EXRULE, event.exrule.getValue());
|
|
|
|
|
if (!event.getExceptions().isEmpty())
|
|
|
|
|
try {
|
|
|
|
|
builder.withValue(Events.EXDATE, DateUtils.recurrenceSetsToAndroidString(event.getExdates(), event.isAllDay()));
|
|
|
|
|
} catch (ParseException e) {
|
|
|
|
|
Log.e(TAG, "Couldn't parse ExDate(s)", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// set either DTEND for single-time events or DURATION for recurring events
|
|
|
|
|
// because that's the way Android likes it (see docs)
|
|
|
|
|
if (recurring) {
|
|
|
|
|
// calculate DURATION from start and end date
|
|
|
|
|
Duration duration = new Duration(event.dtStart.getDate(), event.dtEnd.getDate());
|
|
|
|
|
builder.withValue(Events.DURATION, duration.getValue());
|
|
|
|
|
} else
|
|
|
|
|
builder .withValue(Events.DTEND, event.getDtEndInMillis())
|
|
|
|
|
.withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID());
|
|
|
|
|
|
|
|
|
|
if (event.summary != null)
|
|
|
|
|
builder.withValue(Events.TITLE, event.summary);
|
|
|
|
|
if (event.location != null)
|
|
|
|
|
builder.withValue(Events.EVENT_LOCATION, event.location);
|
|
|
|
|
if (event.description != null)
|
|
|
|
|
builder.withValue(Events.DESCRIPTION, event.description);
|
|
|
|
|
|
|
|
|
|
if (event.organizer != null) {
|
|
|
|
|
final URI uri = event.organizer.getCalAddress();
|
|
|
|
|
String email = null;
|
|
|
|
|
if (uri != null && "mailto".equalsIgnoreCase(uri.getScheme()))
|
|
|
|
|
email = uri.getSchemeSpecificPart();
|
|
|
|
|
else {
|
|
|
|
|
iCalendar.Email emailParam = (iCalendar.Email)event.organizer.getParameter(iCalendar.Email.PARAMETER_NAME);
|
|
|
|
|
if (emailParam != null)
|
|
|
|
|
email = emailParam.getValue();
|
|
|
|
|
}
|
|
|
|
|
if (email != null)
|
|
|
|
|
builder.withValue(Events.ORGANIZER, email);
|
|
|
|
|
else
|
|
|
|
|
Log.w(TAG, "Got ORGANIZER without email address which is not supported by Android, ignoring");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.status!= null) {
|
|
|
|
|
int statusCode = Events.STATUS_TENTATIVE;
|
|
|
|
|
if (event.status == Status.VEVENT_CONFIRMED)
|
|
|
|
|
statusCode = Events.STATUS_CONFIRMED;
|
|
|
|
|
else if (event.status == Status.VEVENT_CANCELLED)
|
|
|
|
|
statusCode = Events.STATUS_CANCELED;
|
|
|
|
|
builder.withValue(Events.STATUS, statusCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder.withValue(Events.AVAILABILITY, event.opaque ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);
|
|
|
|
|
|
|
|
|
|
if (event.forPublic != null)
|
|
|
|
|
builder.withValue(Events.ACCESS_LEVEL, event.forPublic ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);
|
|
|
|
|
|
|
|
|
|
return builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
|
|
|
|
final Event event = (Event)resource;
|
|
|
|
|
|
|
|
|
|
// add attendees
|
|
|
|
|
for (Attendee attendee : event.getAttendees())
|
|
|
|
|
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
|
|
|
|
// add reminders
|
|
|
|
|
for (VAlarm alarm : event.getAlarms())
|
|
|
|
|
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
|
|
|
|
// add exceptions
|
|
|
|
|
for (Event exception : event.getExceptions()) {
|
|
|
|
|
final int backrefIdxEx = pendingOperations.size(); // save exception ID as backref value
|
|
|
|
|
pendingOperations.add(buildException(newDataInsertBuilder(Events.CONTENT_URI, Events.ORIGINAL_ID, localID, backrefIdx), event, exception).build());
|
|
|
|
|
addDataRows(exception, -1, backrefIdxEx); // build attendees and reminders for exception
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void removeDataRows(Resource resource) {
|
|
|
|
|
final Event event = (Event)resource;
|
|
|
|
|
|
|
|
|
|
// delete exceptions
|
|
|
|
|
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());
|
|
|
|
|
// delete reminders
|
|
|
|
|
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
|
|
|
|
.withSelection(Reminders.EVENT_ID + "=?", new String[]{String.valueOf(event.getLocalID())}).build());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected Builder buildException(Builder builder, Event master, Event exception) {
|
|
|
|
|
buildEntry(builder, exception, false);
|
|
|
|
|
|
|
|
|
|
final boolean originalAllDay = master.isAllDay();
|
|
|
|
|
|
|
|
|
|
Date date = exception.recurrenceId.getDate();
|
|
|
|
|
if (originalAllDay && date instanceof DateTime) { // correct VALUE=DATE-TIME RECURRENCE-IDs to VALUE=DATE
|
|
|
|
|
final DateFormat dateFormatDate = new SimpleDateFormat("yyyyMMdd");
|
|
|
|
|
final String dateString = dateFormatDate.format(exception.recurrenceId.getDate());
|
|
|
|
|
try {
|
|
|
|
|
date = new Date(dateString);
|
|
|
|
|
} catch (ParseException e) {
|
|
|
|
|
Log.e(TAG, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder.withValue(Events.ORIGINAL_INSTANCE_TIME, date.getTime());
|
|
|
|
|
builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0);
|
|
|
|
|
return builder;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SuppressLint("InlinedApi")
|
|
|
|
|
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
|
|
|
|
final URI member = attendee.getCalAddress();
|
|
|
|
|
if ("mailto".equalsIgnoreCase(member.getScheme()))
|
|
|
|
|
// attendee identified by email
|
|
|
|
|
builder = builder.withValue(Attendees.ATTENDEE_EMAIL, member.getSchemeSpecificPart());
|
|
|
|
|
else {
|
|
|
|
|
// attendee identified by other URI
|
|
|
|
|
builder = builder
|
|
|
|
|
.withValue(Attendees.ATTENDEE_ID_NAMESPACE, member.getScheme())
|
|
|
|
|
.withValue(Attendees.ATTENDEE_IDENTITY, member.getSchemeSpecificPart());
|
|
|
|
|
iCalendar.Email email = (iCalendar.Email)attendee.getParameter(iCalendar.Email.PARAMETER_NAME);
|
|
|
|
|
if (email != null)
|
|
|
|
|
builder = builder.withValue(Attendees.ATTENDEE_EMAIL, email.getValue());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Cn cn = (Cn)attendee.getParameter(Parameter.CN);
|
|
|
|
|
if (cn != null)
|
|
|
|
|
builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue());
|
|
|
|
|
|
|
|
|
|
int type = Attendees.TYPE_NONE;
|
|
|
|
|
|
|
|
|
|
CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE);
|
|
|
|
|
if (cutype == CuType.RESOURCE || cutype == CuType.ROOM)
|
|
|
|
|
// "attendee" is a (physical) resource
|
|
|
|
|
type = Attendees.TYPE_RESOURCE;
|
|
|
|
|
else {
|
|
|
|
|
// attendee is not a (physical) resource
|
|
|
|
|
Role role = (Role)attendee.getParameter(Parameter.ROLE);
|
|
|
|
|
int relationship;
|
|
|
|
|
if (role == Role.CHAIR)
|
|
|
|
|
relationship = Attendees.RELATIONSHIP_ORGANIZER;
|
|
|
|
|
else {
|
|
|
|
|
relationship = Attendees.RELATIONSHIP_ATTENDEE;
|
|
|
|
|
if (role == Role.OPT_PARTICIPANT)
|
|
|
|
|
type = Attendees.TYPE_OPTIONAL;
|
|
|
|
|
else if (role == Role.REQ_PARTICIPANT)
|
|
|
|
|
type = Attendees.TYPE_REQUIRED;
|
|
|
|
|
}
|
|
|
|
|
builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int status = Attendees.ATTENDEE_STATUS_NONE;
|
|
|
|
|
PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT);
|
|
|
|
|
if (partStat == null || partStat == PartStat.NEEDS_ACTION)
|
|
|
|
|
status = Attendees.ATTENDEE_STATUS_INVITED;
|
|
|
|
|
else if (partStat == PartStat.ACCEPTED)
|
|
|
|
|
status = Attendees.ATTENDEE_STATUS_ACCEPTED;
|
|
|
|
|
else if (partStat == PartStat.DECLINED)
|
|
|
|
|
status = Attendees.ATTENDEE_STATUS_DECLINED;
|
|
|
|
|
else if (partStat == PartStat.TENTATIVE)
|
|
|
|
|
status = Attendees.ATTENDEE_STATUS_TENTATIVE;
|
|
|
|
|
|
|
|
|
|
return builder
|
|
|
|
|
.withValue(Attendees.ATTENDEE_TYPE, type)
|
|
|
|
|
.withValue(Attendees.ATTENDEE_STATUS, status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Builder buildReminder(Builder builder, VAlarm alarm) {
|
|
|
|
|
int minutes = 0;
|
|
|
|
|
|
|
|
|
|
if (alarm.getTrigger() != null) {
|
|
|
|
|
Dur duration = alarm.getTrigger().getDuration();
|
|
|
|
|
if (duration != null) {
|
|
|
|
|
// negative value in TRIGGER means positive value in Reminders.MINUTES and vice versa
|
|
|
|
|
minutes = -(((duration.getWeeks() * 7 + duration.getDays()) * 24 + duration.getHours()) * 60 + duration.getMinutes());
|
|
|
|
|
if (duration.isNegative())
|
|
|
|
|
minutes *= -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "Adding alarm " + minutes + " minutes before");
|
|
|
|
|
return builder
|
|
|
|
|
.withValue(Reminders.METHOD, Reminders.METHOD_ALERT)
|
|
|
|
|
.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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|