at.bitfire.davdroid.mirakel.resource.LocalCalendar.java Source code

Java tutorial

Introduction

Here is the source code for at.bitfire.davdroid.mirakel.resource.LocalCalendar.java

Source

/*******************************************************************************
 * Copyright (c) 2014 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.mirakel.resource;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lombok.Cleanup;
import lombok.Getter;
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.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.Status;

import org.apache.commons.lang.StringUtils;

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.Context;
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.provider.ContactsContract;
import android.util.Log;

/**
 * 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 long id;
    @Getter
    protected String url;

    protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1;

    /* database fields */

    @Override
    protected Uri entriesURI() {
        return syncAdapterURI(Events.CONTENT_URI);
    }

    protected String entryColumnAccountType() {
        return Events.ACCOUNT_TYPE;
    }

    protected String entryColumnAccountName() {
        return Events.ACCOUNT_NAME;
    }

    protected String entryColumnParentID() {
        return Events.CALENDAR_ID;
    }

    protected String entryColumnID() {
        return Events._ID;
    }

    protected String entryColumnRemoteName() {
        return Events._SYNC_ID;
    }

    protected String entryColumnETag() {
        return Events.SYNC_DATA1;
    }

    protected String entryColumnDirty() {
        return Events.DIRTY;
    }

    protected String entryColumnDeleted() {
        return Events.DELETED;
    }

    @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 void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info)
            throws LocalStorageException {
        ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY);
        if (client == null)
            throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)");

        int color = 0xFFC3EA6E; // fallback: "DAVdroid green"
        if (info.getColor() != null) {
            Pattern p = Pattern.compile("#(\\p{XDigit}{6})(\\p{XDigit}{2})?");
            Matcher m = p.matcher(info.getColor());
            if (m.find()) {
                int color_rgb = Integer.parseInt(m.group(1), 16);
                int color_alpha = m.group(2) != null ? (Integer.parseInt(m.group(2), 16) & 0xFF) : 0xFF;
                color = (color_alpha << 24) | color_rgb;
            }
        }

        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, color);
        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, info.getTimezone());

        Log.i(TAG, "Inserting calendar: " + values.toString() + " -> " + calendarsURI(account).toString());
        try {
            client.insert(calendarsURI(account), values);
        } catch (RemoteException e) {
            throw new LocalStorageException(e);
        }
    }

    public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient, Context ctx)
            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<LocalCalendar>();
        while (cursor != null && cursor.moveToNext())
            calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1), ctx));
        return calendars.toArray(new LocalCalendar[0]);
    }

    public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url, Context ctx)
            throws RemoteException {
        super(account, providerClient, ctx);
        this.id = id;
        this.url = url;
    }

    /* 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.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);
        }
    }

    /* create/update/delete */

    public Event newResource(long localID, String resourceName, String eTag) {
        return new Event(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(
                entryColumnParentID() + "=? AND (" + where + ")", new String[] { String.valueOf(id) });
        pendingOperations.add(builder.withYieldAllowed(true).build());
    }

    /* methods for populating the data object from the content provider */

    @Override
    public void populate(Resource resource) throws LocalStorageException {
        Event e = (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 },
                    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);
                }

                // 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);
        }
    }

    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) {
                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);
            }
        }
    }

    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.addAlarm(alarm);
        }
    }

    /* content builder methods */

    @Override
    protected Builder buildEntry(Builder builder, Resource resource, boolean insert) {
        Event event = (Event) resource;

        builder = builder.withValue(Events.CALENDAR_ID, id).withValue(entryColumnRemoteName(), event.getName())
                .withValue(entryColumnETag(), event.getETag()).withValue(entryColumnUID(), event.getUid())
                .withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
                .withValue(Events.DTSTART, event.getDtStartInMillis())
                .withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
                .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);

        boolean recurring = false;
        if (event.getRrule() != null) {
            recurring = true;
            builder = builder.withValue(Events.RRULE, event.getRrule().getValue());
        }
        if (event.getRdate() != null) {
            recurring = true;
            builder = builder.withValue(Events.RDATE, event.getRdate().getValue());
        }
        if (event.getExrule() != null)
            builder = builder.withValue(Events.EXRULE, event.getExrule().getValue());
        if (event.getExdate() != null)
            builder = builder.withValue(Events.EXDATE, event.getExdate().getValue());

        // 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.getDtStart().getDate(), event.getDtEnd().getDate());
            builder = builder.withValue(Events.DURATION, duration.getValue());
        } else {
            builder = builder.withValue(Events.DTEND, event.getDtEndInMillis()).withValue(Events.EVENT_END_TIMEZONE,
                    event.getDtEndTzID());
        }

        if (event.getSummary() != null)
            builder = builder.withValue(Events.TITLE, event.getSummary());
        if (event.getLocation() != null)
            builder = builder.withValue(Events.EVENT_LOCATION, event.getLocation());
        if (event.getDescription() != null)
            builder = builder.withValue(Events.DESCRIPTION, event.getDescription());

        if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) {
            URI organizer = event.getOrganizer().getCalAddress();
            if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto"))
                builder = builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart());
        }

        Status status = event.getStatus();
        if (status != null) {
            int statusCode = Events.STATUS_TENTATIVE;
            if (status == Status.VEVENT_CONFIRMED)
                statusCode = Events.STATUS_CONFIRMED;
            else if (status == Status.VEVENT_CANCELLED)
                statusCode = Events.STATUS_CANCELED;
            builder = builder.withValue(Events.STATUS, statusCode);
        }

        builder = builder.withValue(Events.AVAILABILITY,
                event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE);

        if (event.getForPublic() != null)
            builder = builder.withValue(Events.ACCESS_LEVEL,
                    event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE);

        return builder;
    }

    @Override
    protected void addDataRows(Resource resource, long localID, int backrefIdx) {
        Event event = (Event) resource;
        for (Attendee attendee : event.getAttendees())
            pendingOperations.add(buildAttendee(
                    newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee)
                            .build());
        for (VAlarm alarm : event.getAlarms())
            pendingOperations.add(buildReminder(
                    newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm)
                            .build());
    }

    @Override
    protected void removeDataRows(Resource resource) {
        Event event = (Event) resource;
        pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
                .withSelection(Attendees.EVENT_ID + "=?", new String[] { String.valueOf(event.getLocalID()) })
                .build());
        pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
                .withSelection(Reminders.EVENT_ID + "=?", new String[] { String.valueOf(event.getLocalID()) })
                .build());
    }

    @SuppressLint("InlinedApi")
    protected Builder buildAttendee(Builder builder, Attendee attendee) {
        Uri member = Uri.parse(attendee.getValue());
        String email = member.getSchemeSpecificPart();

        Cn cn = (Cn) attendee.getParameter(Parameter.CN);
        if (cn != null)
            builder = builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue());

        int type = Attendees.TYPE_NONE;

        CuType cutype = (CuType) attendee.getParameter(Parameter.CUTYPE);
        if (cutype == CuType.RESOURCE)
            type = Attendees.TYPE_RESOURCE;
        else {
            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 = 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_EMAIL, email).withValue(Attendees.ATTENDEE_TYPE, type)
                .withValue(Attendees.ATTENDEE_STATUS, status);
    }

    protected Builder buildReminder(Builder builder, VAlarm alarm) {
        int minutes = 0;

        Dur duration;
        if (alarm.getTrigger() != null && (duration = alarm.getTrigger().getDuration()) != null)
            minutes = duration.getDays() * 24 * 60 + duration.getHours() * 60 + duration.getMinutes();

        Log.d(TAG, "Adding alarm " + minutes + " min 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);
    }

}