org.ciasaboark.tacere.database.DatabaseInterface.java Source code

Java tutorial

Introduction

Here is the source code for org.ciasaboark.tacere.database.DatabaseInterface.java

Source

/*
 * Copyright (c) 2015 Jonathan Nelson
 * Released under the BSD license.  For details see the COPYING file.
 */

package org.ciasaboark.tacere.database;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Instances;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import org.acra.ACRA;
import org.ciasaboark.tacere.event.Calendar;
import org.ciasaboark.tacere.event.EventInstance;
import org.ciasaboark.tacere.event.ringer.RingerType;
import org.ciasaboark.tacere.prefs.Prefs;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

import static android.provider.CalendarContract.Calendars;

public class DatabaseInterface {
    private static final String TAG = "DatabaseInterface";
    private static final String[] SYSTEM_DB_PROJECTION = new String[] { Instances.TITLE, Instances.BEGIN,
            Instances.END, Instances.DESCRIPTION, Instances.ALL_DAY, Instances.AVAILABILITY, Instances._ID,
            Instances.CALENDAR_ID, Instances.EVENT_ID, Instances.EVENT_LOCATION, Instances.CALENDAR_COLOR,
            Instances.EVENT_COLOR };
    private static final String[] LOCAL_DB_PROJECTION = new String[] { Columns._ID, Columns.BEGIN,
            Columns.EFFECTIVE_END, Columns.EVENT_ID, Columns.TITLE, Columns.CAL_ID, Columns.DESCRIPTION,
            Columns.DISPLAY_COLOR, Columns.IS_ALLDAY, Columns.IS_FREETIME, Columns.LOCATION, Columns.RINGER_TYPE,
            Columns.EXTEND_BUFFER, Columns.ORGINAL_END };
    private static final int LOCAL_DB_PROJECTION_ID = 0;
    private static final int LOCAL_DB_PROJECTION_BEGIN = 1;
    private static final int LOCAL_DB_PROJECTION_EFFECTIVE_END = 2;
    private static final int LOCAL_DB_PROJECTION_EVENT_ID = 3;
    private static final int LOCAL_DB_PROJECTION_TITLE = 4;
    private static final int LOCAL_DB_PROJECTION_CAL_ID = 5;
    private static final int LOCAL_DB_PROJECTION_DESCRIPTION = 6;
    private static final int LOCAL_DB_PROJECTION_DISPLAY_COLOR = 7;
    private static final int LOCAL_DB_PROJECTION_ALLDAY = 8;
    private static final int LOCAL_DB_PROJECTION_AVAILABLE = 9;
    private static final int LOCAL_DB_PROJECTION_LOCATION = 10;
    private static final int LOCAL_DB_PROJECTION_RINGER = 11;
    private static final int LOCAL_DB_PROJECTION_EXTEND_MINUTES = 12;
    private static final int LOCAL_DB_PROJECTION_ORIGINAL_END = 13;
    private static final long MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;
    private static DatabaseInterface instance;
    private static Context context = null;
    private static Prefs prefs = null;
    private final int SYSTEM_DB_PROJECTION_TITLE = 0;
    private final int SYSTEM_DB_PROJECTION_BEGIN = 1;
    private final int SYSTEM_DB_PROJECTION_END = 2;
    private final int SYSTEM_DB_PROJECTION_DESCRIPTION = 3;
    private final int SYSTEM_DB_PROJECTION_ALL_DAY = 4;
    private final int SYSTEM_DB_PROJECTION_AVAILABLE = 5;
    private final int SYSTEM_DB_PROJECTION_ID = 6;
    private final int SYSTEM_DB_PROJECTION_CAL_ID = 7;
    private final int SYSTEM_DB_PROJECTION_EVENT_ID = 8;
    private final int SYSTEM_DB_PROJECTION_LOCATION = 9;
    private final int SYSTEM_DB_PROJECTION_CALENDAR_COLOR = 10;
    private final int SYSTEM_DB_PROJECTION_EVENT_COLOR = 11;
    private final SQLiteDatabase eventsDB;

    public static DatabaseInterface getInstance(Context ctx) {
        if (ctx == null) {
            throw new IllegalArgumentException("Context can not be null");
        }
        context = ctx;
        if (instance == null) {
            instance = new DatabaseInterface(context);
        }
        if (prefs == null) {
            prefs = new Prefs(context);
        }

        return instance;
    }

    private DatabaseInterface(Context context) {
        DatabaseInterface.context = context;
        EventDatabaseOpenHelper dbHelper = new EventDatabaseOpenHelper(context);
        this.eventsDB = dbHelper.getWritableDatabase();
    }

    private Cursor getEventCursor(long begin, long end) {
        assert begin >= 0;
        assert end >= 0;

        String[] args = { String.valueOf(begin), String.valueOf(end) };
        String selection = Columns.BEGIN + " >= ? AND " + Columns.EFFECTIVE_END + " <= ?";
        Cursor cursor = eventsDB.query(EventDatabaseOpenHelper.TABLE_EVENTS, null, selection, args, null, null,
                Columns.BEGIN, null);
        return cursor;
    }

    public boolean isDatabaseEmpty() {
        boolean isEmpty = false;
        Cursor cursor = getEventCursor();
        if (cursor.getCount() == 0) {
            isEmpty = true;
        }
        cursor.close();
        return isEmpty;
    }

    public Cursor getEventCursor() {
        return getOrderedEventCursor(Columns.BEGIN);
    }

    private Cursor getOrderedEventCursor(String order) {
        return eventsDB.query(EventDatabaseOpenHelper.TABLE_EVENTS, null, null, null, null, null, order, null);
    }

    public void setExtendMinutesForInstance(long instanceId, int extendMinutes) {
        if (extendMinutes < 0) {
            throw new IllegalArgumentException("extendMinutes must be >= 0");
        }
        try {
            EventInstance eventInstance = getEvent(instanceId);
            eventInstance.setExtendMinutes(extendMinutes);
            insertEvent(eventInstance);
        } catch (NoSuchEventInstanceException e) {
            Log.e(TAG, "not able to find event with instance id : " + instanceId + ", can not extend minutes");
        }
    }

    // returns the event that matches the given Instance id, throws NoSuchEventException if no match
    public EventInstance getEvent(long instanceId) throws NoSuchEventInstanceException {
        String whereClause = Columns._ID + " = ?";
        String[] whereArgs = new String[] { String.valueOf(instanceId) };
        EventInstance thisEvent = null;
        Cursor cursor = null;
        try {
            cursor = eventsDB.query(EventDatabaseOpenHelper.TABLE_EVENTS, LOCAL_DB_PROJECTION, whereClause,
                    whereArgs, null, null, null);

            if (cursor.moveToFirst()) {
                do {
                    int id = cursor.getInt(cursor.getColumnIndex(Columns._ID));
                    if (id == instanceId) {
                        long cal_id = cursor.getInt(cursor.getColumnIndex(Columns.CAL_ID));
                        int event_id = cursor.getInt(cursor.getColumnIndex(Columns.EVENT_ID));
                        String title = cursor.getString(cursor.getColumnIndex(Columns.TITLE));
                        long begin = cursor.getLong(cursor.getColumnIndex(Columns.BEGIN));
                        long originalEnd = cursor.getLong(cursor.getColumnIndex(Columns.ORGINAL_END));
                        String description = cursor.getString(cursor.getColumnIndex(Columns.DESCRIPTION));
                        int ringerInt = cursor.getInt(cursor.getColumnIndex(Columns.RINGER_TYPE));
                        int displayColor = cursor.getInt(cursor.getColumnIndex(Columns.DISPLAY_COLOR));
                        boolean isFreeTime = cursor.getInt(cursor.getColumnIndex(Columns.IS_FREETIME)) == 1;
                        boolean isAllDay = cursor.getInt(cursor.getColumnIndex(Columns.IS_ALLDAY)) == 1;
                        String location = cursor.getString(cursor.getColumnIndex(Columns.LOCATION));
                        int extendBuffer = cursor.getInt(cursor.getColumnIndex(Columns.EXTEND_BUFFER));

                        thisEvent = new EventInstance(cal_id, id, event_id, title, begin, originalEnd, description,
                                displayColor, isFreeTime, isAllDay, extendBuffer);
                        RingerType ringerType = RingerType.getTypeForInt(ringerInt);
                        thisEvent.setRingerType(ringerType);
                        thisEvent.setLocation(location);

                        break;
                    }
                } while (cursor.moveToNext());
            }
        } catch (SQLiteException e) {
            Log.e(TAG, "caught SQLiteException while trying to get local database cursor: " + e.getMessage());
            Log.w(TAG, "sending ARCA bug report");
            ACRA.getErrorReporter().handleSilentException(null);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        if (thisEvent == null) {
            throw new NoSuchEventInstanceException(TAG + " can not find event with given id " + instanceId);
        }
        return thisEvent;
    }

    public void insertEvent(EventInstance e) {
        if (!isEventValidToInsert(e)) {
            throw new IllegalArgumentException("DatabaseInterface:insertEvent given an event with blank values");
        }

        ContentValues cv = new ContentValues();
        cv.put(Columns._ID, e.getId());
        cv.put(Columns.TITLE, e.getTitle());
        cv.put(Columns.BEGIN, e.getBegin());
        cv.put(Columns.ORGINAL_END, e.getOriginalEnd());
        cv.put(Columns.DESCRIPTION, e.getDescription());
        cv.put(Columns.IS_ALLDAY, e.isAllDay());
        cv.put(Columns.IS_FREETIME, e.isFreeTime());
        cv.put(Columns.RINGER_TYPE, e.getRingerType().value);
        cv.put(Columns.DISPLAY_COLOR, e.getDisplayColor());
        cv.put(Columns.CAL_ID, e.getCalendarId());
        cv.put(Columns.EVENT_ID, e.getEventId());
        cv.put(Columns.LOCATION, e.getLocation());
        cv.put(Columns.EXTEND_BUFFER, e.getExtendMinutes());
        cv.put(Columns.EFFECTIVE_END, e.getEffectiveEnd());

        long rowID = eventsDB.insertWithOnConflict(EventDatabaseOpenHelper.TABLE_EVENTS, null, cv,
                SQLiteDatabase.CONFLICT_REPLACE);
        Log.d(TAG, "inserted event " + e.toString() + " as row " + rowID);
    }

    private boolean isEventValidToInsert(EventInstance e) {
        boolean eventIsValid = false;
        if (e != null && e.getTitle() != null && e.getId() >= 0 && e.getBegin() != null
                && e.getOriginalEnd() != null && e.isFreeTime() != null && e.isAllDay() != null) {
            eventIsValid = true;
        }
        return eventIsValid;
    }

    public void setRingerForInstance(long instanceId, RingerType ringerType) {
        if (ringerType == null) {
            throw new IllegalArgumentException("ringerType must not be null");
        }

        String mSelectionClause = Columns._ID + " = ?";
        String[] mSelectionArgs = { String.valueOf(instanceId) };
        ContentValues values = new ContentValues();
        values.put(Columns.RINGER_TYPE, ringerType.value);
        eventsDB.beginTransaction();
        try {
            int rowsUpdated = eventsDB.update(EventDatabaseOpenHelper.TABLE_EVENTS, values, mSelectionClause,
                    mSelectionArgs);
            if (rowsUpdated != 1) {
                throw new Exception("setRingerForInstance() should have updated 1 row for instance id " + instanceId
                        + ", updated " + rowsUpdated);
            } else {
                Log.d(TAG, "set new ringer '" + ringerType + "' for instance id " + instanceId);
            }
            eventsDB.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e(TAG, "error setting ringer type: " + e.getMessage());
            e.printStackTrace();
        } finally {
            eventsDB.endTransaction();
        }
    }

    public Deque<EventInstance> getAllActiveEvents() {
        //TODO better SQL select
        Deque<EventInstance> events = new ArrayDeque<EventInstance>();
        String whereClause = Columns.EFFECTIVE_END + " > ? AND " + Columns.BEGIN + " < ?";
        long beginTime = System.currentTimeMillis()
                - (EventInstance.MILLISECONDS_IN_MINUTE * (long) prefs.getBufferMinutes());
        long endTime = System.currentTimeMillis()
                + (EventInstance.MILLISECONDS_IN_MINUTE * (long) prefs.getBufferMinutes());
        String[] whereArgs = new String[] { String.valueOf(beginTime), String.valueOf(endTime) };
        Cursor cursor = null;

        try {
            cursor = eventsDB.query(EventDatabaseOpenHelper.TABLE_EVENTS, LOCAL_DB_PROJECTION, whereClause,
                    whereArgs, null, null, null);

            while (cursor.moveToNext()) {
                long id = cursor.getLong(LOCAL_DB_PROJECTION_ID);
                long calId = cursor.getLong(LOCAL_DB_PROJECTION_CAL_ID);
                long eventId = cursor.getLong(LOCAL_DB_PROJECTION_EVENT_ID);
                long begin = cursor.getLong(LOCAL_DB_PROJECTION_BEGIN);
                long end = cursor.getLong(LOCAL_DB_PROJECTION_ORIGINAL_END);
                String title = cursor.getString(LOCAL_DB_PROJECTION_TITLE);
                String description = cursor.getString(LOCAL_DB_PROJECTION_DESCRIPTION);
                String location = cursor.getString(LOCAL_DB_PROJECTION_LOCATION);
                int displayColor = cursor.getInt(LOCAL_DB_PROJECTION_DISPLAY_COLOR);
                boolean isAllDay = cursor.getInt(LOCAL_DB_PROJECTION_ALLDAY) == 1;
                boolean isAvailable = cursor.getInt(LOCAL_DB_PROJECTION_AVAILABLE) == 1;
                int ringerInt = cursor.getInt(LOCAL_DB_PROJECTION_RINGER);
                RingerType ringerType = RingerType.getTypeForInt(ringerInt);
                int extendMinutes = cursor.getInt(LOCAL_DB_PROJECTION_EXTEND_MINUTES);

                EventInstance e = new EventInstance(calId, id, eventId, title, begin, end, description,
                        displayColor, isAvailable, isAllDay, extendMinutes);
                e.setLocation(location);
                e.setRingerType(ringerType);
                if (e.isActiveBetween(beginTime, endTime)) {
                    events.add(e);
                }

            }
        } catch (Exception e) {
            Log.d(TAG, "error getting cursor for active events");
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return events;
    }

    public void setRingerForAllInstancesOfEvent(long eventId, RingerType ringerType) {
        if (ringerType == null) {
            throw new IllegalArgumentException("ringerType can not be null");
        }

        String mSelectionClause = Columns.EVENT_ID + " = ?";
        String[] mSelectionArgs = { String.valueOf(eventId) };
        ContentValues values = new ContentValues();
        values.put(Columns.RINGER_TYPE, ringerType.value);
        eventsDB.beginTransaction();
        try {
            int rowsUpdated = eventsDB.update(EventDatabaseOpenHelper.TABLE_EVENTS, values, mSelectionClause,
                    mSelectionArgs);
            Log.d(TAG, "setRingerForAllInstancesOfEvent updated " + rowsUpdated + " for event id " + eventId);
            eventsDB.setTransactionSuccessful();
        } catch (Exception e) {
            Log.e(TAG, "setRingerForAllInstancesOfEvent() error setting ringer type: " + e.getMessage()
                    + " for event id " + eventId + ", aborting");
            e.printStackTrace();
        } finally {
            eventsDB.endTransaction();
        }
    }

    /**
     * Sync the internal database with the system calendar database. Forward syncing is limited to
     * the period specified in preferences. Old events are pruned.
     */
    public void syncCalendarDb() throws SecurityException {
        if (!isCalendarPermissionGranted()) {
            //user has not yet given access to calendar
            Log.e(TAG, "no permission to read system calendar, database sync aborted");
            return;
        } else {
            // update(n) will also remove all events not found in the next n days, so we
            // + need to keep this in sync with the users preferences.
            int lookaheadDays = prefs.getLookaheadDays().value;
            update(lookaheadDays);
            pruneEventsEndBefore(System.currentTimeMillis()
                    - EventInstance.MILLISECONDS_IN_MINUTE * (long) prefs.getBufferMinutes());
            pruneEventsBeginAfter(System.currentTimeMillis() + (lookaheadDays * EventInstance.MILLISECONDS_IN_DAY));
            pruneEventsFromRemovedCalendars();
            pruneEventsRemovedFromSystemCalendar();
        }
    }

    private boolean isCalendarPermissionGranted() {
        return ContextCompat.checkSelfPermission(context,
                android.Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED;
    }

    // sync the calendar and the local database for the given number of days
    private void update(int days) {
        if (days < 0) {
            throw new IllegalArgumentException("can not sync for a negative period of days: " + days);
        }

        long begin = System.currentTimeMillis();
        long end = begin + MILLISECONDS_IN_DAY * (long) days; // pull all events n days from now

        Cursor calendarCursor = getCalendarCursor(begin, end);
        if (calendarCursor == null) {
            Log.e(TAG, "unable to get system cursor, update process will not continue");
            Log.w(TAG, "sending ACRA bug report");
            ACRA.getErrorReporter().handleSilentException(null);
        } else {
            List calendarsToSync = prefs.getSelectedCalendarsIds();

            if (calendarCursor.moveToFirst()) {
                do {
                    // the cursor
                    String event_title = calendarCursor.getString(SYSTEM_DB_PROJECTION_TITLE);
                    long event_begin = calendarCursor.getLong(SYSTEM_DB_PROJECTION_BEGIN);
                    long event_end = calendarCursor.getLong(SYSTEM_DB_PROJECTION_END);
                    String event_description = calendarCursor.getString(SYSTEM_DB_PROJECTION_DESCRIPTION);
                    int event_displayColor; //use the event color if possible, else use calendar color
                    String eventColorString = calendarCursor.getString(SYSTEM_DB_PROJECTION_EVENT_COLOR);
                    if (eventColorString != null) {
                        event_displayColor = calendarCursor.getInt(SYSTEM_DB_PROJECTION_EVENT_COLOR);
                    } else {
                        event_displayColor = calendarCursor.getInt(SYSTEM_DB_PROJECTION_CALENDAR_COLOR);
                    }
                    int event_allDay = calendarCursor.getInt(SYSTEM_DB_PROJECTION_ALL_DAY);
                    int event_availability = calendarCursor.getInt(SYSTEM_DB_PROJECTION_AVAILABLE);
                    long id = calendarCursor.getLong(SYSTEM_DB_PROJECTION_ID);
                    long cal_id = calendarCursor.getLong(SYSTEM_DB_PROJECTION_CAL_ID);
                    long event_id = calendarCursor.getLong(SYSTEM_DB_PROJECTION_EVENT_ID);
                    String event_location = calendarCursor.getString(SYSTEM_DB_PROJECTION_LOCATION);
                    boolean isEventAllDay = event_allDay == 1;
                    boolean isEventAvailable = event_availability == Instances.AVAILABILITY_FREE;

                    EventInstance newEvent = new EventInstance(cal_id, id, event_id, event_title, event_begin,
                            event_end, event_description, event_displayColor, isEventAvailable, isEventAllDay, 0);
                    newEvent.setLocation(event_location);

                    // if the event is already in the local database then we need to preserve
                    // the ringerType, all other values should be read from the system calendar
                    // database
                    try {
                        EventInstance oldEvent = getEvent(id);
                        RingerType oldRinger = oldEvent.getRingerType();
                        int oldExtendMinutes = oldEvent.getExtendMinutes();
                        newEvent.setRingerType(oldRinger);
                        newEvent.setExtendMinutes(oldExtendMinutes);
                    } catch (NoSuchEventInstanceException e) {
                        // its perfectly reasonable that this event does not exist within our database
                        // yet
                    }

                    // inserting an event with the same id will clobber all previous data, completing
                    // the synchronization of this event

                    long calendarId = newEvent.getCalendarId();
                    if (prefs.shouldAllCalendarsBeSynced() || calendarsToSync.contains(calendarId)) {
                        insertEvent(newEvent);
                    } else {
                        removeEventIfExists(newEvent);
                    }
                } while (calendarCursor.moveToNext());
            }
            calendarCursor.close();
        }
    }

    private void pruneEventsEndBefore(long time) {
        String whereClause = Columns.EFFECTIVE_END + " < ?";
        String[] args = new String[] { String.valueOf(time) };
        int rowsDeleted = eventsDB.delete(EventDatabaseOpenHelper.TABLE_EVENTS, whereClause, args);
        Log.d(TAG, "pruned " + rowsDeleted + " events that end before " + time);
    }

    private void pruneEventsBeginAfter(long time) {
        String whereClause = Columns.BEGIN + " > ?";
        String[] args = new String[] { String.valueOf(time) };
        int rowsDeleted = eventsDB.delete(EventDatabaseOpenHelper.TABLE_EVENTS, whereClause, args);
        Log.d(TAG, "pruned " + rowsDeleted + " events that begin after " + time);
    }

    private void pruneEventsFromRemovedCalendars() {
        if (!prefs.shouldAllCalendarsBeSynced()) {
            List<Long> calendarIds = prefs.getSelectedCalendarsIds();

            Iterator i = calendarIds.iterator();
            int index = 0;
            String idList = "";
            while (i.hasNext()) {
                String instanceId = String.valueOf(i.next());
                idList += " '" + instanceId + "'";
                index++;
                if (i.hasNext()) {
                    idList += ", ";
                }
            }
            String whereClause = Columns.CAL_ID + " NOT IN (" + idList + ")";

            try {
                int deletedRowCount = eventsDB.delete(EventDatabaseOpenHelper.TABLE_EVENTS, whereClause, null);
                Log.d(TAG, "deleted " + deletedRowCount + " events that belonged to calendars "
                        + "that should not be synced");
            } catch (Exception e) {
                Log.e(TAG, "error deleting events not in system calendar: " + e.getMessage());
            }

        }
    }

    private void pruneEventsRemovedFromSystemCalendar() {
        //remove local events that no longer exist within the system calendar
        List<Long> systemCalendarInstanceIds = getInstanceIdsFromSystemDatabase();
        List<Long> localInstanceIds = getInstanceIdsFromLocalDatabase();
        for (Long id : localInstanceIds) {
            if (!systemCalendarInstanceIds.contains(id)) {
                deleteEventWithId(id);
            }
        }
    }

    private Cursor getCalendarCursor(long begin, long end) {
        return Instances.query(context.getContentResolver(), SYSTEM_DB_PROJECTION, begin, end);
    }

    private void removeEventIfExists(EventInstance event) {
        long instanceId = event.getId();
        String whereClause = Columns._ID + "=?";
        String[] whereArgs = new String[] { String.valueOf(instanceId) };
        int rowsDeleted = eventsDB.delete(EventDatabaseOpenHelper.TABLE_EVENTS, whereClause, whereArgs);
        Log.d(TAG, "deleted " + rowsDeleted + " rows");
    }

    private List<Long> getInstanceIdsFromSystemDatabase() {
        List<Long> systemInstanceIds = new ArrayList<Long>();
        int lookaheadDays = prefs.getLookaheadDays().value;
        long begin = System.currentTimeMillis();
        long end = System.currentTimeMillis() + (lookaheadDays * EventInstance.MILLISECONDS_IN_DAY);
        Cursor calCursor = null;
        try {
            calCursor = getCalendarCursor(begin, end);
            while (calCursor.moveToNext()) {
                long instanceId = calCursor.getLong(SYSTEM_DB_PROJECTION_ID);
                systemInstanceIds.add(instanceId);
            }
        } catch (Exception e) {
            Log.e(TAG, "error getting list of instance ids from system calendar");
        } finally {
            if (calCursor != null) {
                calCursor.close();
            }
        }
        return systemInstanceIds;
    }

    private List<Long> getInstanceIdsFromLocalDatabase() {
        List<Long> localInstanceIds = new ArrayList<Long>();
        Cursor localCursor = getEventCursor();
        while (localCursor.moveToNext()) {
            long id = localCursor.getLong(LOCAL_DB_PROJECTION_ID);
            localInstanceIds.add(id);
        }
        //TODO
        return localInstanceIds;
    }

    private void deleteEventWithId(long id) {
        String selection = Columns._ID + " = ?";
        eventsDB.delete(EventDatabaseOpenHelper.TABLE_EVENTS, selection, new String[] { String.valueOf(id) });
    }

    public List<Long> getInstanceIdsForEvent(long eventId) {
        List<Long> events = new ArrayList<Long>();
        //TODO make faster with select query to avoid event id check
        Cursor cursor = getEventCursor();
        if (cursor.moveToFirst()) {
            do {
                long _eventId = cursor.getLong(cursor.getColumnIndex(Columns.EVENT_ID));
                if (eventId == _eventId) {
                    long instanceId = cursor.getLong(cursor.getColumnIndex(Columns._ID));
                    events.add(instanceId);
                }
            } while (cursor.moveToNext());
        }
        cursor.close();
        return events;
    }

    public String getCalendarNameForId(long id) {
        String calendarName = "";
        if (isCalendarPermissionGranted()) {
            final String[] projection = { Calendars._ID, Calendars.NAME, Calendars.CALENDAR_DISPLAY_NAME };
            final int projection_id = 0;
            final int projection_name = 1;
            final int projection_display_name = 2;

            Cursor cursor;
            ContentResolver cr = context.getContentResolver();
            cursor = cr.query(Calendars.CONTENT_URI, projection, null, null, null);
            if (cursor.moveToFirst()) {
                do {
                    long calendarId = cursor.getLong(projection_id);
                    if (calendarId == id) {
                        calendarName = cursor.getString(projection_name);
                        if (calendarName == null || calendarName == "") {
                            calendarName = cursor.getString(projection_display_name);
                            if (calendarName == null)
                                calendarName = "";
                        }
                        break;
                    }
                } while (cursor.moveToNext());
            }
            cursor.close();
        }
        return calendarName;
    }

    public List<Calendar> getCalendarIdList() {
        List<Calendar> calendarIds = new ArrayList<Calendar>();
        if (isCalendarPermissionGranted()) {
            final String[] projection = { Calendars._ID, Calendars.ACCOUNT_NAME, Calendars.CALENDAR_DISPLAY_NAME,
                    Calendars.OWNER_ACCOUNT, Calendars.CALENDAR_COLOR };
            final int projection_id = 0;
            final int projection_accountName = 1;
            final int projection_displayname = 2;
            final int projection_owner = 3;
            final int projection_color = 4;
            Cursor cursor;
            ContentResolver cr = context.getContentResolver();
            //TODO wrap in try/catch/finally close
            cursor = cr.query(CalendarContract.Calendars.CONTENT_URI, projection, null, null, null);
            if (cursor.moveToFirst()) {
                do {
                    long id = cursor.getLong(projection_id);
                    String accountName = cursor.getString(projection_accountName);
                    String displayName = cursor.getString(projection_displayname);
                    String owner = cursor.getString(projection_owner);
                    int color = cursor.getInt(projection_color);
                    try {
                        Calendar c = new Calendar(id, accountName, displayName, owner, color);
                        calendarIds.add(c);
                    } catch (IllegalArgumentException e) {
                        Log.w(TAG,
                                "android database supplied bad values calendar info for id " + id + ", accountName:"
                                        + accountName + "displayName:" + displayName + " owner:" + owner);
                        Log.w(TAG, e.getMessage());
                    }
                } while (cursor.moveToNext());
            } else {
                Log.d(TAG, "no calendars installed");
            }
            cursor.close();
        }

        return calendarIds;
    }

    public long getRemainingEventRepetitionCount(long eventId) {
        return getEventRepetitionCountFromTime(eventId, System.currentTimeMillis());
    }

    public long getEventRepetitionCountFromTime(long eventId, long begin) {
        long eventRepetitions = 0;
        boolean querySucceeded = false;
        int tryCount = 0;
        while (!querySucceeded && tryCount < 20) {
            try {
                eventRepetitions = tryGetEventRepetitionCountFromTime(eventId, begin);
                querySucceeded = true;
            } catch (Exception e) {
                Log.w(TAG, "Error getting repetition count for event with id " + eventId + " on attempt number "
                        + tryCount);
            }
        }

        return eventRepetitions;
    }

    private long tryGetEventRepetitionCountFromTime(long eventId, long begin) throws Exception {
        //the local database can not be trusted to return an accurate count of the event repetions
        //since its window is limited.  Instead we have to query the system database
        long eventRepetitions = 0;

        final String[] EVENT_PROJECTION = new String[] { Instances.EVENT_ID, Instances._ID, Instances.TITLE };

        ContentResolver cr = context.getContentResolver();
        Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
        ContentUris.appendId(builder, begin);
        ContentUris.appendId(builder, Long.MAX_VALUE);

        String selection = Instances.EVENT_ID + " = ?";
        String[] selectionArgs = { String.valueOf(eventId) };

        Cursor cursor = null;
        try {
            cursor = cr.query(builder.build(), EVENT_PROJECTION, selection, selectionArgs, null);

            eventRepetitions = cursor.getCount();
        } catch (Exception e) {
            Log.e(TAG, "error getting repetition count for event with id " + eventId);
            throw new Exception("Error performing query");
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return eventRepetitions;
    }

    public boolean doesEventRepeat(long eventId) {
        boolean eventRepeats = false;
        if (getEventRepetitionCount(eventId) > 1) {
            eventRepeats = true;
        }
        return eventRepeats;
    }

    public long getEventRepetitionCount(long eventId) {
        return getEventRepetitionCountFromTime(eventId, 0);
    }

    /**
     * Query the internal database for the next event.
     *
     * @return the next event in the database
     * @throws NoSuchEventInstanceException if there is no next event
     */
    public EventInstance nextEvent() {
        EventInstance nextEvent = null;

        Cursor cursor = getOrderedEventCursor(Columns.BEGIN);
        if (cursor.moveToFirst()) {
            int id = cursor.getInt(cursor.getColumnIndex(Columns._ID));
            try {
                nextEvent = getEvent(id);
            } catch (NoSuchEventInstanceException e) {
                Log.e(TAG, "unable to retrieve event with id of " + id
                        + " even though a record with this id exists in the database");
            }
        }
        cursor.close();

        return nextEvent;
    }

}