FileDocCategorySizeDatePackage
CalendarProvider.javaAPI DocAndroid 1.5 API179761Wed May 06 22:42:48 BST 2009com.android.providers.calendar

CalendarProvider

public class CalendarProvider extends android.content.AbstractSyncableContentProvider

Fields Summary
private static final boolean
PROFILE
private static final boolean
MULTIPLE_ATTENDEES_PER_EVENT
private static final String[]
ACCOUNTS_PROJECTION
private static final String[]
EVENTS_PROJECTION
private static final int
EVENTS_SYNC_ID_INDEX
private static final int
EVENTS_SYNC_VERSION_INDEX
private static final int
EVENTS_SYNC_ACCOUNT_INDEX
private static final int
EVENTS_CALENDAR_ID_INDEX
private static final int
EVENTS_RRULE_INDEX
private static final int
EVENTS_RDATE_INDEX
private static final int
EVENTS_ORIGINAL_EVENT_INDEX
private DatabaseUtils.InsertHelper
mCalendarsInserter
private DatabaseUtils.InsertHelper
mEventsInserter
private DatabaseUtils.InsertHelper
mEventsRawTimesInserter
private DatabaseUtils.InsertHelper
mDeletedEventsInserter
private DatabaseUtils.InsertHelper
mInstancesInserter
private DatabaseUtils.InsertHelper
mAttendeesInserter
private DatabaseUtils.InsertHelper
mRemindersInserter
private DatabaseUtils.InsertHelper
mCalendarAlertsInserter
private DatabaseUtils.InsertHelper
mExtendedPropertiesInserter
MetaData
mMetaData
The cached copy of the CalendarMetaData database table. Make this "package private" instead of "private" so that test code can access it.
private static final int
BUSYBIT_INTERVAL
private static final int[]
BIT_MASKS
private static final long
SCHEDULE_ALARM_SLACK
We search backward in time for event reminders that we may have missed and schedule them if the event has not yet expired. The amount in the past to search backwards is controlled by this constant. It should be at least a few minutes to allow for an event that was recently created on the web to make its way to the phone. Two hours might seem like overkill, but it is useful in the case where the user just crossed into a new timezone and might have just missed an alarm.
private static final long
CLEAR_OLD_ALARM_THRESHOLD
Alarms older than this threshold will be deleted from the CalendarAlerts table. This should be at least a day because if the timezone is wrong and the user corrects it we might delete good alarms that appear to be old because the device time was incorrectly in the future. This threshold must also be larger than SCHEDULE_ALARM_SLACK. We add the SCHEDULE_ALARM_SLACK to ensure this. To make it easier to find and debug problems with missed reminders, set this to something greater than a day.
private Object
mAlarmLock
private static final String
TAG
private static final String
DATABASE_NAME
private static final int
DATABASE_VERSION
private static final String
EXPECTED_PROJECTION
private static final String
DESIRED_PROJECTION
private static final String
FEEDS_SUBSTRING
private static final long
MINIMUM_EXPANSION_SPAN
private static final String[]
sCalendarsIdProjection
private static final int
CALENDARS_INDEX_ID
private static final String
CALENDAR_ID_SELECTION
private static final String[]
sInstancesProjection
private static final int
INSTANCES_INDEX_START_DAY
private static final int
INSTANCES_INDEX_END_DAY
private static final int
INSTANCES_INDEX_START_MINUTE
private static final int
INSTANCES_INDEX_END_MINUTE
private static final int
INSTANCES_INDEX_ALL_DAY
private static final String[]
sBusyBitProjection
private static final int
BUSYBIT_INDEX_DAY
private static final int
BUSYBIT_INDEX_BUSYBITS
private static final int
BUSYBIT_INDEX_ALL_DAY_COUNT
private com.google.wireless.gdata.calendar.client.CalendarClient
mCalendarClient
private android.app.AlarmManager
mAlarmManager
private CalendarSyncAdapter
mSyncAdapter
private CalendarAppWidgetProvider
mAppWidgetProvider
private android.content.BroadcastReceiver
mIntentReceiver
Listens for timezone changes and disk-no-longer-full events
private static final String[]
EXPAND_COLUMNS
private static String
sEventsTable
private static android.net.Uri
sEventsURL
private static String
sDeletedEventsTable
private static android.net.Uri
sDeletedEventsURL
private static String
sAttendeesTable
private static String
sRemindersTable
private static String
sCalendarAlertsTable
private static String
sExtendedPropertiesTable
private static final int
EVENTS
private static final int
EVENTS_ID
private static final int
INSTANCES
private static final int
DELETED_EVENTS
private static final int
CALENDARS
private static final int
CALENDARS_ID
private static final int
ATTENDEES
private static final int
ATTENDEES_ID
private static final int
REMINDERS
private static final int
REMINDERS_ID
private static final int
EXTENDED_PROPERTIES
private static final int
EXTENDED_PROPERTIES_ID
private static final int
CALENDAR_ALERTS
private static final int
CALENDAR_ALERTS_ID
private static final int
CALENDAR_ALERTS_BY_INSTANCE
private static final int
BUSYBITS
private static final android.content.UriMatcher
sURLMatcher
private static final HashMap
sInstancesProjectionMap
private static final HashMap
sEventsProjectionMap
private static final HashMap
sAttendeesProjectionMap
private static final HashMap
sRemindersProjectionMap
private static final HashMap
sCalendarAlertsProjectionMap
private static final HashMap
sBusyBitsProjectionMap
Constructors Summary
public CalendarProvider()


      
        super(DATABASE_NAME, DATABASE_VERSION, Calendars.CONTENT_URI);
    
Methods Summary
private voidacquireBusyBitRange(int startDay, int endDay)
Expands the Instances table (if needed) and the BusyBits table. Acquires the database lock and calls {@link #acquireBusyBitRangeLocked}.

        mDb.beginTransaction();
        try {
            acquireBusyBitRangeLocked(startDay, endDay);
            mDb.setTransactionSuccessful();
        } finally {
            mDb.endTransaction();
        }
    
private voidacquireBusyBitRangeLocked(int firstDay, int lastDay)

        if (firstDay > lastDay) {
            throw new IllegalArgumentException("firstDay must not be greater than lastDay");
        }
        String localTimezone = TimeZone.getDefault().getID();
        MetaData.Fields fields = mMetaData.getFieldsLocked();
        String dbTimezone = fields.timezone;
        int minBusyBit = fields.minBusyBit;
        int maxBusyBit = fields.maxBusyBit;
        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
        if (firstDay >= minBusyBit && lastDay <= maxBusyBit && !timezoneChanged) {
            if (Config.LOGV) {
                Log.v(TAG, "acquireBusyBitRangeLocked() no expansion needed");
            }
            return;
        }

        // Avoid gaps in the BusyBit table and avoid recomputing the busy bits
        // that are already in the table.  If the busy bit range has been cleared,
        // don't bother checking.
        if (maxBusyBit != 0) {
            if (firstDay > maxBusyBit) {
                firstDay = maxBusyBit;
            } else if (lastDay < minBusyBit) {
                lastDay = minBusyBit;
            } else if (firstDay < minBusyBit && lastDay <= maxBusyBit) {
                lastDay = minBusyBit;
            } else if (lastDay > maxBusyBit && firstDay >= minBusyBit) {
                firstDay = maxBusyBit;
            }
        }

        // Allocate space for the busy bits, one 32-bit integer for each day.
        int numDays = lastDay - firstDay + 1;
        int[] busybits = new int[numDays];
        int[] allDayCounts = new int[numDays];

        // Convert the first and last Julian day range to a range that uses
        // UTC milliseconds.
        Time time = new Time();
        long begin = time.setJulianDay(firstDay);

        // We add one to lastDay because the time is set to 12am on the given
        // Julian day and we want to include all the events on the last day.
        long end = time.setJulianDay(lastDay + 1);

        // Make sure the Instances table includes events in the range
        // [begin, end].
        acquireInstanceRange(begin, end, true /* use minimum expansion window */);

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
                "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
        qb.setProjectionMap(sInstancesProjectionMap);
        qb.appendWhere("begin <= ");
        qb.appendWhere(String.valueOf(end));
        qb.appendWhere(" AND end >= ");
        qb.appendWhere(String.valueOf(begin));
        qb.appendWhere(" AND ");
        qb.appendWhere(Instances.SELECTED);
        qb.appendWhere("=1");

        final SQLiteDatabase db = getDatabase();
        // Get all the instances that overlap the range [begin,end]
        Cursor cursor = qb.query(db, sInstancesProjection, null, null, null, null, null);
        int count = 0;
        try {
            count = cursor.getCount();
            while (cursor.moveToNext()) {
                int startDay = cursor.getInt(INSTANCES_INDEX_START_DAY);
                int endDay = cursor.getInt(INSTANCES_INDEX_END_DAY);
                int startMinute = cursor.getInt(INSTANCES_INDEX_START_MINUTE);
                int endMinute = cursor.getInt(INSTANCES_INDEX_END_MINUTE);
                boolean allDay = cursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
                fillBusyBits(firstDay, startDay, endDay, startMinute, endMinute,
                        allDay, busybits, allDayCounts);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        if (count == 0) {
            return;
        }

        // Read the busybit range again because that may have changed when we
        // called acquireInstanceRange().
        fields = mMetaData.getFieldsLocked();
        minBusyBit = fields.minBusyBit;
        maxBusyBit = fields.maxBusyBit;

        // If the busybit range was cleared, then delete all the entries.
        if (maxBusyBit == 0) {
            mDb.execSQL("DELETE FROM BusyBits;");
        }

        // Merge the busy bits with the database.
        mergeBusyBits(firstDay, lastDay, busybits, allDayCounts);
        if (maxBusyBit == 0) {
            minBusyBit = firstDay;
            maxBusyBit = lastDay;
        } else {
            if (firstDay < minBusyBit) {
                minBusyBit = firstDay;
            }
            if (lastDay > maxBusyBit) {
                maxBusyBit = lastDay;
            }
        }
        // Update the busy bit range
        mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
                minBusyBit, maxBusyBit);
    
private voidacquireInstanceRange(long begin, long end, boolean useMinimumExpansionWindow)
Ensure that the date range given has all elements in the instance table. Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.

        mDb.beginTransaction();
        try {
            acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow);
            mDb.setTransactionSuccessful();
        } finally {
            mDb.endTransaction();
        }
    
private voidacquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow)
Ensure that the date range given has all elements in the instance table. The database lock must be held when calling this method.

        long expandBegin = begin;
        long expandEnd = end;

        if (useMinimumExpansionWindow) {
            // if we end up having to expand events into the instances table, expand
            // events for a minimal amount of time, so we do not have to perform
            // expansions frequently.
            long span = end - begin;
            if (span < MINIMUM_EXPANSION_SPAN) {
                long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
                expandBegin -= additionalRange;
                expandEnd += additionalRange;
            }
        }

        // Check if the timezone has changed.
        // We do this check here because the database is locked and we can
        // safely delete all the entries in the Instances table.
        MetaData.Fields fields = mMetaData.getFieldsLocked();
        String dbTimezone = fields.timezone;
        long maxInstance = fields.maxInstance;
        long minInstance = fields.minInstance;
        String localTimezone = TimeZone.getDefault().getID();
        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);

        if (maxInstance == 0 || timezoneChanged) {
            // Empty the Instances table and expand from scratch.
            mDb.execSQL("DELETE FROM Instances;");
            mDb.execSQL("DELETE FROM BusyBits;");
            if (Config.LOGV) {
                Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances and Busybits,"
                        + " timezone changed: " + timezoneChanged);
            }
            expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone);

            mMetaData.writeLocked(localTimezone, expandBegin, expandEnd,
                    0 /* startDay */, 0 /* endDay */);
            return;
        }

        // If the desired range [begin, end] has already been
        // expanded, then simply return.  The range is inclusive, that is,
        // events that touch either endpoint are included in the expansion.
        // This means that a zero-duration event that starts and ends at
        // the endpoint will be included.
        // We use [begin, end] here and not [expandBegin, expandEnd] for
        // checking the range because a common case is for the client to
        // request successive days or weeks, for example.  If we checked
        // that the expanded range [expandBegin, expandEnd] then we would
        // always be expanding because there would always be one more day
        // or week that hasn't been expanded.
        if ((begin >= minInstance) && (end <= maxInstance)) {
            if (Config.LOGV) {
                Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
                        + ") falls within previously expanded range.");
            }
            return;
        }

        // If the requested begin point has not been expanded, then include
        // more events than requested in the expansion (use "expandBegin").
        if (begin < minInstance) {
            expandInstanceRangeLocked(expandBegin, minInstance, localTimezone);
            minInstance = expandBegin;
        }

        // If the requested end point has not been expanded, then include
        // more events than requested in the expansion (use "expandEnd").
        if (end > maxInstance) {
            expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone);
            maxInstance = expandEnd;
        }

        // Update the bounds on the Instances table.
        mMetaData.writeLocked(localTimezone, minInstance, maxInstance,
                fields.minBusyBit, fields.maxBusyBit);
    
voidbootCompleted()

        // Remove alarms from the CalendarAlerts table that have been marked
        // as "scheduled" but not fired yet.  We do this because the
        // AlarmManagerService loses all information about alarms when the
        // power turns off but we store the information in a database table
        // that persists across reboots. See the documentation for
        // scheduleNextAlarmLocked() for more information.
        scheduleNextAlarm(true /* remove alarms */);
    
protected voidbootstrapDatabase(android.database.sqlite.SQLiteDatabase db)

        super.bootstrapDatabase(db);
        db.execSQL("CREATE TABLE Calendars (" +
                        "_id INTEGER PRIMARY KEY," +
                        "_sync_account TEXT," +
                        "_sync_id TEXT," +
                        "_sync_version TEXT," +
                        "_sync_time TEXT," +            // UTC
                        "_sync_local_id INTEGER," +
                        "_sync_dirty INTEGER," +
                        "_sync_mark INTEGER," + // Used to filter out new rows
                        "url TEXT," +
                        "name TEXT," +
                        "displayName TEXT," +
                        "hidden INTEGER NOT NULL DEFAULT 0," +
                        "color INTEGER," +
                        "access_level INTEGER," +
                        "selected INTEGER NOT NULL DEFAULT 1," +
                        "sync_events INTEGER NOT NULL DEFAULT 0," +
                        "location TEXT," +
                        "timezone TEXT" +
                        ");");

        // Trigger to remove a calendar's events when we delete the calendar
        db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
                    "BEGIN " +
                        "DELETE FROM Events WHERE calendar_id = old._id;" +
                        "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
                    "END");

        // TODO: do we need both dtend and duration?
        db.execSQL("CREATE TABLE Events (" +
                        "_id INTEGER PRIMARY KEY," +
                        "_sync_account TEXT," +
                        "_sync_id TEXT," +
                        "_sync_version TEXT," +
                        "_sync_time TEXT," +            // UTC
                        "_sync_local_id INTEGER," +
                        "_sync_dirty INTEGER," +
                        "_sync_mark INTEGER," + // To filter out new rows
                        "calendar_id INTEGER NOT NULL," +
                        "htmlUri TEXT," +
                        "title TEXT," +
                        "eventLocation TEXT," +
                        "description TEXT," +
                        "eventStatus INTEGER," +
                        "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
                        "commentsUri TEXT," +
                        "dtstart INTEGER," +               // millis since epoch
                        "dtend INTEGER," +                 // millis since epoch
                        "eventTimezone TEXT," +         // timezone for event
                        "duration TEXT," +
                        "allDay INTEGER NOT NULL DEFAULT 0," +
                        "visibility INTEGER NOT NULL DEFAULT 0," +
                        "transparency INTEGER NOT NULL DEFAULT 0," +
                        "hasAlarm INTEGER NOT NULL DEFAULT 0," +
                        "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
                        "rrule TEXT," +
                        "rdate TEXT," +
                        "exrule TEXT," +
                        "exdate TEXT," +
                        "originalEvent TEXT," +  // _sync_id of recurring event
                        "originalInstanceTime INTEGER," +  // millis since epoch
                        "originalAllDay INTEGER," +
                        "lastDate INTEGER" +               // millis since epoch
                    ");");

        db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
                + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");

        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
                   Events.CALENDAR_ID +
                   ");");

        db.execSQL("CREATE TABLE EventsRawTimes (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER NOT NULL," +
                        "dtstart2445 TEXT," +
                        "dtend2445 TEXT," +
                        "originalInstanceTime2445 TEXT," +
                        "lastDate2445 TEXT," +
                        "UNIQUE (event_id)" +
                    ");");

        // NOTE: we do not create a trigger to delete an event's instances upon update,
        // as all rows currently get updated during a merge.

        db.execSQL("CREATE TABLE DeletedEvents (" +
                        "_sync_id TEXT," +
                        "_sync_version TEXT," +
                        "_sync_account TEXT," +
                        (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
                        "_sync_mark INTEGER," + // To filter out new rows
                        "calendar_id INTEGER" +
                    ");");

        db.execSQL("CREATE TABLE Instances (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER," +
                        "begin INTEGER," +         // UTC millis
                        "end INTEGER," +           // UTC millis
                        "startDay INTEGER," +      // Julian start day
                        "endDay INTEGER," +        // Julian end day
                        "startMinute INTEGER," +   // minutes from midnight
                        "endMinute INTEGER," +     // minutes from midnight
                        "UNIQUE (event_id, begin, end)" +
                    ");");

        db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
                   Instances.START_DAY +
                   ");");

        db.execSQL("CREATE TABLE CalendarMetaData (" +
                        "_id INTEGER PRIMARY KEY," +
                        "localTimezone TEXT," +
                        "minInstance INTEGER," +      // UTC millis
                        "maxInstance INTEGER," +      // UTC millis
                        "minBusyBits INTEGER," +      // UTC millis
                        "maxBusyBits INTEGER" +       // UTC millis
        ");");

        db.execSQL("CREATE TABLE BusyBits(" +
                        "day INTEGER PRIMARY KEY," +  // the Julian day
                        "busyBits INTEGER," +         // 24 bits for 60-minute intervals
                        "allDayCount INTEGER" +       // number of all-day events
        ");");

        db.execSQL("CREATE TABLE Attendees (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER," +
                        "attendeeName TEXT," +
                        "attendeeEmail TEXT," +
                        "attendeeStatus INTEGER," +
                        "attendeeRelationship INTEGER," +
                        "attendeeType INTEGER" +
                   ");");

        db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
                   Attendees.EVENT_ID +
                   ");");

        db.execSQL("CREATE TABLE Reminders (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER," +
                        "minutes INTEGER," +
                        "method INTEGER NOT NULL" +
                        " DEFAULT " + Reminders.METHOD_DEFAULT +
                   ");");

        db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
                   Reminders.EVENT_ID +
                   ");");

        // This table stores the Calendar notifications that have gone off.
        db.execSQL("CREATE TABLE CalendarAlerts (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER," +
                        "begin INTEGER NOT NULL," +         // UTC millis
                        "end INTEGER NOT NULL," +           // UTC millis
                        "alarmTime INTEGER NOT NULL," +     // UTC millis
                        "creationTime INTEGER NOT NULL," +  // UTC millis
                        "receivedTime INTEGER NOT NULL," +  // UTC millis
                        "notifyTime INTEGER NOT NULL," +    // UTC millis
                        "state INTEGER NOT NULL," +
                        "minutes INTEGER," +
                        "UNIQUE (alarmTime, begin, event_id)" +
                   ");");

        db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
                   CalendarAlerts.EVENT_ID +
                   ");");

        db.execSQL("CREATE TABLE ExtendedProperties (" +
                        "_id INTEGER PRIMARY KEY," +
                        "event_id INTEGER," +
                        "name TEXT," +
                        "value TEXT" +
                   ");");

        db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
                   ExtendedProperties.EVENT_ID +
                   ");");

        // Trigger to remove data tied to an event when we delete that event.
        db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
                    "BEGIN " +
                        "DELETE FROM Instances WHERE event_id = old._id;" +
                        "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
                        "DELETE FROM Attendees WHERE event_id = old._id;" +
                        "DELETE FROM Reminders WHERE event_id = old._id;" +
                        "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
                        "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
                    "END");

        // Triggers to set the _sync_dirty flag when an attendee is changed,
        // inserted or deleted
        db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");

        // Triggers to set the _sync_dirty flag when a reminder is changed,
        // inserted or deleted
        db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");
        // Triggers to set the _sync_dirty flag when an extended property is changed,
        // inserted or deleted
        db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
                    "END");
        db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
                    "BEGIN " +
                        "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
                    "END");
    
longcalculateLastDate(android.content.ContentValues values)

        // Allow updates to some event fields like the title or hasAlarm
        // without requiring DTSTART.
        if (!values.containsKey(Events.DTSTART)) {
            if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
                    || values.containsKey(Events.DURATION)
                    || values.containsKey(Events.EVENT_TIMEZONE)
                    || values.containsKey(Events.RDATE)
                    || values.containsKey(Events.EXRULE)
                    || values.containsKey(Events.EXDATE)) {
                throw new RuntimeException("DTSTART field missing from event");
            }
            return -1;
        }
        long dtstartMillis = values.getAsLong(Events.DTSTART);
        long lastMillis = -1;

        // Can we use dtend with a repeating event?  What does that even
        // mean?
        // NOTE: if the repeating event has a dtend, we convert it to a
        // duration during event processing, so this situation should not
        // occur.
        Long dtEnd = values.getAsLong(Events.DTEND);
        if (dtEnd != null) {
            lastMillis = dtEnd;
        } else {
            // find out how long it is
            Duration duration = new Duration();
            String durationStr = values.getAsString(Events.DURATION);
            if (durationStr != null) {
                duration.parse(durationStr);
            }

            RecurrenceSet recur = new RecurrenceSet(values);

            if (recur.hasRecurrence()) {
                // the event is repeating, so find the last date it
                // could appear on

                String tz = values.getAsString(Events.EVENT_TIMEZONE);

                if (TextUtils.isEmpty(tz)) {
                    // floating timezone
                    tz = Time.TIMEZONE_UTC;
                }
                Time dtstartLocal = new Time(tz);

                dtstartLocal.set(dtstartMillis);

                RecurrenceProcessor rp = new RecurrenceProcessor();
                lastMillis = rp.getLastOccurence(dtstartLocal, recur);
                if (lastMillis == -1) {
                    return lastMillis;  // -1
                }
            } else {
                // the event is not repeating, just use dtstartMillis
                lastMillis = dtstartMillis;
            }

            // that was the beginning of the event.  this is the end.
            lastMillis = duration.addTo(lastMillis);
        }
        return lastMillis;
    
private java.lang.StringcalendarEmailAddressFromFeedUrl(java.lang.String feed)
Extracts the calendar email from a calendar feed url.

param
feed the calendar feed url
return
the calendar email that is in the feed url or null if it can't find the email address.

        // Example feed url:
        // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
        String[] pathComponents = feed.split("/");
        if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
            try {
                return URLDecoder.decode(pathComponents[5], "UTF-8");
            } catch (UnsupportedEncodingException e) {
                Log.e(TAG, "unable to url decode the email address in calendar " + feed);
                return null;
            }
        }

        Log.e(TAG, "unable to find the email address in calendar " + feed);
        return null;
    
private java.lang.StringcalendarEntryToContentValues(java.lang.String account, com.google.wireless.gdata.calendar.data.CalendarsFeed feed, com.google.wireless.gdata.calendar.data.CalendarEntry entry, android.content.ContentValues map)
Convert the CalenderEntry to a Bundle that can be inserted/updated into the Calendars table.

        map.clear();

        String url = entry.getAlternateLink();

        // we only want to fetch the full-selfattendance calendar feeds
        if (!TextUtils.isEmpty(url)) {
            if (url.endsWith(EXPECTED_PROJECTION)) {
                url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
            }
        } else {
            // yuck.  the alternate link was not available.  we should
            // reconstruct from the id.
            url = entry.getId();
            if (!TextUtils.isEmpty(url)) {
                url = convertCalendarIdToFeedUrl(url);
            } else {
                if (Config.LOGV) {
                    Log.v(TAG, "Cannot generate url for calendar feed.");
                }
                return null;
            }
        }

        url = CalendarSyncAdapter.rewriteUrlforAccount(account, url);

        map.put(Calendars.URL, url);
        map.put(Calendars.NAME, entry.getTitle());

        // TODO:
        map.put(Calendars.DISPLAY_NAME, entry.getTitle());

        map.put(Calendars.TIMEZONE, entry.getTimezone());

        String colorStr = entry.getColor();
        if (!TextUtils.isEmpty(colorStr)) {
            int color = Color.parseColor(colorStr);
            // Ensure the alpha is set to max
            color |= 0xff000000;
            map.put(Calendars.COLOR, color);
        }

        map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);

        map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);

        int accesslevel;
        switch (entry.getAccessLevel()) {
        case CalendarEntry.ACCESS_NONE:
            accesslevel = Calendars.NO_ACCESS;
            break;
        case CalendarEntry.ACCESS_READ:
            accesslevel = Calendars.READ_ACCESS;
            break;
        case CalendarEntry.ACCESS_FREEBUSY:
            accesslevel = Calendars.FREEBUSY_ACCESS;
            break;
        case CalendarEntry.ACCESS_EDITOR:
            accesslevel = Calendars.EDITOR_ACCESS;
            break;
        case CalendarEntry.ACCESS_OWNER:
            accesslevel = Calendars.OWNER_ACCESS;
            break;
        default:
            accesslevel = Calendars.NO_ACCESS;
        }
        map.put(Calendars.ACCESS_LEVEL, accesslevel);
        // TODO: use the update time, when calendar actually supports this.
        // right now, calendar modifies the update time frequently.
        map.put(Calendars._SYNC_TIME, System.currentTimeMillis());

        return url;
    
private voidcomputeTimezoneDependentFields(long begin, long end, android.text.format.Time local, android.content.ContentValues values)
Computes the timezone-dependent fields of an instance of an event and updates the "values" map to contain those fields.

param
begin the start time of the instance (in UTC milliseconds)
param
end the end time of the instance (in UTC milliseconds)
param
local a Time object with the timezone set to the local timezone
param
values a map that will contain the timezone-dependent fields

        local.set(begin);
        int startDay = Time.getJulianDay(begin, local.gmtoff);
        int startMinute = local.hour * 60 + local.minute;

        local.set(end);
        int endDay = Time.getJulianDay(end, local.gmtoff);
        int endMinute = local.hour * 60 + local.minute;

        // Special case for midnight, which has endMinute == 0.  Change
        // that to +24 hours on the previous day to make everything simpler.
        // Exception: if start and end minute are both 0 on the same day,
        // then leave endMinute alone.
        if (endMinute == 0 && endDay > startDay) {
            endMinute = 24 * 60;
            endDay -= 1;
        }

        values.put(Instances.START_DAY, startDay);
        values.put(Instances.END_DAY, endDay);
        values.put(Instances.START_MINUTE, startMinute);
        values.put(Instances.END_MINUTE, endMinute);
    
protected static final java.lang.StringconvertCalendarIdToFeedUrl(java.lang.String url)

        // id: http://www.google.com/calendar/feeds/<username>/<cal id>
        // desired feed:
        //   http://www.google.com/calendar/feeds/<cal id>/<projection>
        int start = url.indexOf(FEEDS_SUBSTRING);
        if (start != -1) {
            // strip out the */ in /feeds/*/
            start += FEEDS_SUBSTRING.length();
            int end = url.indexOf('/", start);
            if (end != -1) {
                url = url.replace(url.substring(start, end + 1), "");
            }
            url = url + "/private" + DESIRED_PROJECTION;
        }
        return url;
    
private voidcreateAttendeeEntry(long eventId, int status)
Creates an entry in the Attendees table that refers to the given event and that has the given response status.

param
eventId the event id that the new entry in the Attendees table should refer to
param
status the response status

        ContentValues values = new ContentValues();
        values.put(Attendees.EVENT_ID, eventId);
        values.put(Attendees.ATTENDEE_STATUS, status);
        values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
        values.put(Attendees.ATTENDEE_RELATIONSHIP,
                Attendees.RELATIONSHIP_ATTENDEE);

        // Get the calendar id for this event
        Cursor cursor = null;
        long calId;
        try {
            cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
                    new String[] { Events.CALENDAR_ID },
                    null /* selection */,
                    null /* selectionArgs */,
                    null /* sort */);
            if (cursor == null) {
                return;
            }
            cursor.moveToFirst();
            calId = cursor.getLong(0);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        // Get the email address of this user from this Calendar
        String emailAddress = null;
        cursor = null;
        try {
            cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
                    new String[] { "_sync_account" },
                    null /* selection */,
                    null /* selectionArgs */,
                    null /* sort */);
            if (cursor == null) {
                return;
            }
            cursor.moveToFirst();
            emailAddress = cursor.getString(0);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
        
        // We don't know the ATTENDEE_NAME but that will be filled in by the
        // server and sent back to us.
        mAttendeesInserter.insert(values);
    
private voiddeleteBusyBitsLocked(long eventId)
This method is called just before an event is deleted.

param
eventId

        MetaData.Fields fields = mMetaData.getFieldsLocked();
        if (fields.maxBusyBit == 0) {
            return;
        }

        // TODO: if the event being deleted is not a recurring event and the
        // start and end time are outside the BusyBit range, then we could
        // avoid clearing the BusyBits table.  For now, always clear the
        // BusyBits table because deleting events is relatively rare.
        mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
                0 /* startDay */, 0 /* endDay */);
    
public intdeleteInternal(android.net.Uri url, java.lang.String where, java.lang.String[] whereArgs)

        final SQLiteDatabase db = getDatabase();
        int match = sURLMatcher.match(url);
        switch (match)
        {
            case EVENTS_ID:
            {
                String id = url.getLastPathSegment();
                if (where != null) {
                    throw new UnsupportedOperationException("CalendarProvider "
                            + "doesn't support where based deletion for type "
                            + match);
                }
                if (!isTemporary()) {
                    deleteBusyBitsLocked(Integer.parseInt(id));

                    // Query this event to get the fields needed for inserting
                    // a new row in the DeletedEvents table.
                    Cursor cursor = db.query("Events", EVENTS_PROJECTION,
                            "_id=" + id, null, null, null, null);
                    try {
                        if (cursor.moveToNext()) {
                            String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
                            if (!TextUtils.isEmpty(syncId)) {
                                String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
                                String syncAccount = cursor.getString(EVENTS_SYNC_ACCOUNT_INDEX);
                                Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);

                                ContentValues values = new ContentValues();
                                values.put(Events._SYNC_ID, syncId);
                                values.put(Events._SYNC_VERSION, syncVersion);
                                values.put(Events._SYNC_ACCOUNT, syncAccount);
                                values.put(Events.CALENDAR_ID, calId);
                                mDeletedEventsInserter.insert(values);

                                // TODO: we may also want to delete exception
                                // events for this event (in case this was a
                                // recurring event).  We can do that with the
                                // following code:
                                // db.delete("Events", "originalEvent=?", new String[] {syncId});
                            }

                            // If this was a recurring event or a recurrence
                            // exception, then force a recalculation of the
                            // instances.
                            String rrule = cursor.getString(EVENTS_RRULE_INDEX);
                            String rdate = cursor.getString(EVENTS_RDATE_INDEX);
                            String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
                            if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
                                    || !TextUtils.isEmpty(origEvent)) {
                                mMetaData.clearInstanceRange();
                            }
                        }
                    } finally {
                        cursor.close();
                        cursor = null;
                    }
                    triggerAppWidgetUpdate(-1);
                }

                // There is a delete trigger that will cause all instances
                // matching this event id to get deleted as well.  In fact, all
                // of the following tables will remove entries matching this
                // event id: Instances, EventsRawTimes, Attendees, Reminders,
                // CalendarAlerts, and ExtendedProperties.
                int result = db.delete("Events", "_id=" + id, null);
                return result;
            }
            case ATTENDEES_ID:
            {
              // we currently don't support deletions to the attendees list.
              // TODO: remove this restriction when we handle the full attendees
              // feed.  we'll need to put in some logic to check that the
              // modification will be allowed by the server.
              throw new IllegalArgumentException("Cannot delete attendees.");
              //                String id = url.getPathSegments().get(1);
              //                int result = db.delete("Attendees", "_id="+id, null);
              //                return result;
            }
            case REMINDERS:
            {
                int result = db.delete("Reminders", where, whereArgs);
                return result;
            }
            case REMINDERS_ID:
            {
                String id = url.getLastPathSegment();
                int result = db.delete("Reminders", "_id="+id, null);
                return result;
            }
            case CALENDAR_ALERTS:
            {
                int result = db.delete("CalendarAlerts", where, whereArgs);
                return result;
            }
            case CALENDAR_ALERTS_ID:
            {
                String id = url.getLastPathSegment();
                int result = db.delete("CalendarAlerts", "_id="+id, null);
                return result;
            }
            case DELETED_EVENTS:
            case EVENTS:
                throw new UnsupportedOperationException("Cannot delete that URL");
            case CALENDARS_ID:
                StringBuilder whereSb = new StringBuilder("_id=");
                whereSb.append(url.getPathSegments().get(1));
                if (!TextUtils.isEmpty(where)) {
                    whereSb.append(" AND (");
                    whereSb.append(where);
                    whereSb.append(')");
                }
                where = whereSb.toString();
                // fall through to CALENDARS for the actual delete
            case CALENDARS:
                return deleteMatchingCalendars(where);
            case INSTANCES:
                throw new UnsupportedOperationException("Cannot delete that URL");
            default:
                throw new IllegalArgumentException("Unknown URL " + url);
        }
    
private intdeleteMatchingCalendars(java.lang.String where)

        // query to find all the calendars that match, for each
        // - delete calendar subscription
        // - delete calendar

        int numDeleted = 0;
        final SQLiteDatabase db = getDatabase();
        Cursor c = db.query("Calendars", sCalendarsIdProjection, where, null,
                null, null, null);
        if (c == null) {
            return 0;
        }
        try {
            while (c.moveToNext()) {
                long id = c.getLong(CALENDARS_INDEX_ID);
                if (!isTemporary()) {
                    modifyCalendarSubscription(id, false /* not selected */);
                }
                c.deleteRow();
                numDeleted++;
            }
        } finally {
            c.close();
        }
        return numDeleted;
    
private voiddoUpdateTimezoneDependentFields()
This method runs in a background thread. If the timezone has changed then the Instances table will be regenerated.

        MetaData.Fields fields = mMetaData.getFields();
        String localTimezone = TimeZone.getDefault().getID();
        if (TextUtils.equals(fields.timezone, localTimezone)) {
            // Even if the timezone hasn't changed, check for missed alarms.
            // This code executes when the CalendarProvider is created and
            // helps to catch missed alarms when the Calendar process is
            // killed (because of low-memory conditions) and then restarted.
            rescheduleMissedAlarms();
            return;
        }

        // The database timezone is different from the current timezone.
        // Regenerate the Instances table for this month.  Include events
        // starting at the beginning of this month.
        long now = System.currentTimeMillis();
        Time time = new Time();
        time.set(now);
        time.monthDay = 1;
        time.hour = 0;
        time.minute = 0;
        time.second = 0;
        long begin = time.normalize(true);
        long end = begin + MINIMUM_EXPANSION_SPAN;
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        handleInstanceQuery(qb, begin, end, new String[] { Instances._ID },
                null /* selection */, null /* sort */);

        // Also pre-compute the BusyBits table for this month.
        int startDay = Time.getJulianDay(begin, time.gmtoff);
        int endDay = startDay + 31;
        qb = new SQLiteQueryBuilder();
        handleBusyBitsQuery(qb, startDay, endDay, sBusyBitProjection,
                null /* selection */, null /* sort */);
        rescheduleMissedAlarms();
    
private voiddropTables(android.database.sqlite.SQLiteDatabase db)

        db.execSQL("DROP TABLE IF EXISTS Calendars;");
        db.execSQL("DROP TABLE IF EXISTS Events;");
        db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
        db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
        db.execSQL("DROP TABLE IF EXISTS Instances;");
        db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
        db.execSQL("DROP TABLE IF EXISTS BusyBits;");
        db.execSQL("DROP TABLE IF EXISTS Attendees;");
        db.execSQL("DROP TABLE IF EXISTS Reminders;");
        db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
        db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
    
private voidexpandInstanceRangeLocked(long begin, long end, java.lang.String localTimezone)
Make instances for the given range.


               
            

        if (PROFILE) {
            Debug.startMethodTracing("expandInstanceRangeLocked");
        }

        final SQLiteDatabase db = getDatabase();
        Cursor entries = null;

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Expanding events between " + begin + " and " + end);
        }

        try {
            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
            qb.setProjectionMap(sEventsProjectionMap);

            String beginString = String.valueOf(begin);
            String endString = String.valueOf(end);

            qb.appendWhere("(dtstart <= ");
            qb.appendWhere(endString);
            qb.appendWhere(" AND ");
            qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
            qb.appendWhere(beginString);
            qb.appendWhere(")) OR (");
            // grab recurrence exceptions that fall outside our expansion window but modify
            // recurrences that do fall within our window.  we won't insert these into the output
            // set of instances, but instead will just add them to our cancellations list, so we
            // can cancel the correct recurrence expansion instances.
            qb.appendWhere("originalInstanceTime IS NOT NULL ");
            qb.appendWhere("AND originalInstanceTime <= ");
            qb.appendWhere(endString);
            qb.appendWhere(" AND ");
            // we don't have originalInstanceDuration or end time.  for now, assume the original
            // instance lasts no longer than 1 week.
            // TODO: compute the originalInstanceEndTime or get this from the server.
            qb.appendWhere("originalInstanceTime >= ");
            qb.appendWhere(String.valueOf(begin - 7*24*60*60*1000 /* 1 week */));
            qb.appendWhere(")");

            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Retrieving events to expand: " + qb.toString());
            }

            entries = qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);

            RecurrenceProcessor rp = new RecurrenceProcessor();

            int statusColumn = entries.getColumnIndex(Events.STATUS);
            int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
            int dtendColumn = entries.getColumnIndex(Events.DTEND);
            int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
            int durationColumn = entries.getColumnIndex(Events.DURATION);
            int rruleColumn = entries.getColumnIndex(Events.RRULE);
            int rdateColumn = entries.getColumnIndex(Events.RDATE);
            int exruleColumn = entries.getColumnIndex(Events.EXRULE);
            int exdateColumn = entries.getColumnIndex(Events.EXDATE);
            int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
            int idColumn = entries.getColumnIndex(Events._ID);
            int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
            int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
            int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);

            ContentValues initialValues;
            EventInstancesMap instancesMap = new EventInstancesMap();

            Duration duration = new Duration();
            Time eventTime = new Time();

            while (entries.moveToNext()) {
                initialValues = null;

                boolean allDay = entries.getInt(allDayColumn) != 0;

                String eventTimezone = entries.getString(eventTimezoneColumn);
                if (allDay || TextUtils.isEmpty(eventTimezone)) {
                  // in the events table, allDay events start at midnight.
                  // this forces them to stay at midnight for all day events
                  // TODO: check that this actually does the right thing.
                  eventTimezone = Time.TIMEZONE_UTC;
                }

                long dtstartMillis = entries.getLong(dtstartColumn);
                Long eventId = Long.valueOf(entries.getLong(idColumn));

                String durationStr = entries.getString(durationColumn);
                if (durationStr != null) {
                    try {
                        duration.parse(durationStr);
                    }
                    catch (DateException e) {
                        Log.w(TAG, "error parsing duration for event "
                                + eventId + "'" + durationStr + "'", e);
                        duration.sign = 1;
                        duration.weeks = 0;
                        duration.days = 0;
                        duration.hours = 0;
                        duration.minutes = 0;
                        duration.seconds = 0;
                        durationStr = "+P0S";
                    }
                }

                String syncId = entries.getString(syncIdColumn);
                String originalEvent = entries.getString(originalEventColumn);

                long originalInstanceTimeMillis = -1;
                if (!entries.isNull(originalInstanceTimeColumn)) {
                    originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
                }
                int status = entries.getInt(statusColumn);

                String rruleStr = entries.getString(rruleColumn);
                String rdateStr = entries.getString(rdateColumn);
                String exruleStr = entries.getString(exruleColumn);
                String exdateStr = entries.getString(exdateColumn);

                RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);

                if (recur.hasRecurrence()) {
                    // the event is repeating

                    if (status == Events.STATUS_CANCELED) {
                        // should not happen!
                        Log.e(TAG, "Found canceled recurring event in "
                        + "Events table.  Ignoring.");
                        continue;
                    }

                    // need to parse the event into a local calendar.
                    eventTime.timezone = eventTimezone;
                    eventTime.set(dtstartMillis);
                    eventTime.allDay = allDay;

                    if (durationStr == null) {
                        // should not happen.
                        Log.e(TAG, "Repeating event has no duration -- "
                                + "should not happen.");
                        if (allDay) {
                            // set to one day.
                            duration.sign = 1;
                            duration.weeks = 0;
                            duration.days = 1;
                            duration.hours = 0;
                            duration.minutes = 0;
                            duration.seconds = 0;
                            durationStr = "+P1D";
                        } else {
                            // compute the duration from dtend, if we can.
                            // otherwise, use 0s.
                            duration.sign = 1;
                            duration.weeks = 0;
                            duration.days = 0;
                            duration.hours = 0;
                            duration.minutes = 0;
                            if (!entries.isNull(dtendColumn)) {
                                long dtendMillis = entries.getLong(dtendColumn);
                                duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
                                durationStr = "+P" + duration.seconds + "S";
                            } else {
                                duration.seconds = 0;
                                durationStr = "+P0S";
                            }
                        }
                    }

                    long[] dates;
                    try {
                        dates = rp.expand(eventTime, recur, begin, end);
                    } catch (DateException e) {
                        Log.w(TAG, "RecurrenceProcessor.expand skipping " + recur, e);
                        continue;
                    } catch (TimeFormatException e) {
                        Log.w(TAG, "RecurrenceProcessor.expand skipping " + recur, e);
                        continue;
                    }

                    // Initialize the "eventTime" timezone outside the loop.
                    // This is used in computeTimezoneDependentFields().
                    if (allDay) {
                        eventTime.timezone = Time.TIMEZONE_UTC;
                    } else {
                        eventTime.timezone = localTimezone;
                    }

                    long durationMillis = duration.getMillis();
                    for (long date : dates) {
                        initialValues = new ContentValues();
                        initialValues.put(Instances.EVENT_ID, eventId);

                        initialValues.put(Instances.BEGIN, date);
                        long dtendMillis = date + durationMillis;
                        initialValues.put(Instances.END, dtendMillis);

                        computeTimezoneDependentFields(date, dtendMillis,
                                eventTime, initialValues);
                        instancesMap.add(syncId, initialValues);
                    }
                } else {
                    // the event is not repeating
                    initialValues = new ContentValues();

                    // if this event has an "original" field, then record
                    // that we need to cancel the original event (we can't
                    // do that here because the order of this loop isn't
                    // defined)
                    if (originalEvent != null && originalInstanceTimeMillis != -1) {
                        initialValues.put(Events.ORIGINAL_EVENT, originalEvent);
                        initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
                                originalInstanceTimeMillis);
                        initialValues.put(Events.STATUS, status);
                    }

                    long dtendMillis = dtstartMillis;
                    if (durationStr == null) {
                        if (!entries.isNull(dtendColumn)) {
                            dtendMillis = entries.getLong(dtendColumn);
                        }
                    } else {
                        dtendMillis = duration.addTo(dtstartMillis);
                    }

                    // this non-recurring event might be a recurrence exception that doesn't
                    // actually fall within our expansion window, but instead was selected
                    // so we can correctly cancel expanded recurrence instances below.  do not
                    // add events to the instances map if they don't actually fall within our
                    // expansion window.
                    if ((dtendMillis < begin) || (dtstartMillis > end)) {
                        initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
                    }

                    initialValues.put(Instances.EVENT_ID, eventId);
                    initialValues.put(Instances.BEGIN, dtstartMillis);

                    initialValues.put(Instances.END, dtendMillis);

                    if (allDay) {
                        eventTime.timezone = Time.TIMEZONE_UTC;
                    } else {
                        eventTime.timezone = localTimezone;
                    }
                    computeTimezoneDependentFields(dtstartMillis, dtendMillis,
                            eventTime, initialValues);

                    instancesMap.add(syncId, initialValues);
                }
            }

            // First, delete the original instances corresponding to recurrence
            // exceptions.  We do this by iterating over the list and for each
            // recurrence exception, we search the list for an instance with a
            // matching "original instance time".  If we find such an instance,
            // we remove it from the list.  If we don't find such an instance
            // then we cancel the recurrence exception.
            Set<String> keys = instancesMap.keySet();
            for (String syncId : keys) {
                InstancesList list = instancesMap.get(syncId);
                for (ContentValues values : list) {

                    // If this instance is not a recurrence exception, then
                    // skip it.
                    if (!values.containsKey(Events.ORIGINAL_EVENT)) {
                        continue;
                    }

                    String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
                    long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
                     InstancesList originalList = instancesMap.get(originalEvent);
                    if (originalList == null) {
                        // The original recurrence is not present, so don't try canceling it.
                        continue;
                    }

                    // Search the original event for a matching original
                    // instance time.  If there is a matching one, then remove
                    // the original one.  We do this both for exceptions that
                    // change the original instance as well as for exceptions
                    // that delete the original instance.
                    boolean found = false;
                    for (int num = originalList.size() - 1; num >= 0; num--) {
                        ContentValues originalValues = originalList.get(num);
                        long beginTime = originalValues.getAsLong(Instances.BEGIN);
                        if (beginTime == originalTime) {
                            // We found the original instance, so remove it.
                            found = true;
                            originalList.remove(num);
                        }
                    }

                    // If we didn't find a matching instance time then cancel
                    // this recurrence exception.
                    if (!found) {
                        values.put(Events.STATUS, Events.STATUS_CANCELED);
                    }
                }
            }

            // Now do the inserts.  Since the db lock is held when this method is executed,
            // this will be done in a transaction.
            // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
            // while the calendar app is trying to query the db (expanding instances)), we will
            // not be "polite" and yield the lock until we're done.  This will favor local query
            // operations over sync/write operations.
            for (String syncId : keys) {
                InstancesList list = instancesMap.get(syncId);
                for (ContentValues values : list) {

                    // If this instance was cancelled then don't create a new
                    // instance.
                    Integer status = values.getAsInteger(Events.STATUS);
                    if (status != null && status == Events.STATUS_CANCELED) {
                        continue;
                    }

                    // Remove these fields before inserting a new instance
                    values.remove(Events.ORIGINAL_EVENT);
                    values.remove(Events.ORIGINAL_INSTANCE_TIME);
                    values.remove(Events.STATUS);
                    
                    mInstancesInserter.replace(values);
                    if (false) {
                        // yield the lock if anyone else is trying to
                        // perform a db operation here.
                        db.yieldIfContended();
                    }
                }
            }
        } catch (TimeFormatException e) {
            Log.w(TAG, "Exception in instance query preparation", e);
        }
        finally {
            if (entries != null) {
                entries.close();
            }
        }
        if (PROFILE) {
            Debug.stopMethodTracing();
        }
        //System.out.println("EXIT  insertInstanceRange begin=" + begin + " end=" + end);
    
private voidfetchCalendarsFromServer()

        if (mCalendarClient == null) {
            Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
            return;
        }

        GoogleLoginServiceBlockingHelper loginHelper = null;
        String username = null;
        String authToken = null;

        try {
            loginHelper = new GoogleLoginServiceBlockingHelper(getContext());

            // TODO: allow caller to specify which account's feeds should be updated
            username = loginHelper.getAccount(false);
            if (TextUtils.isEmpty(username)) {
                Log.w(TAG, "Unable to update calendars from server -- "
                      + "no users configured.");
                return;
            }

            try {
                authToken = loginHelper.getAuthToken(username,
                                                     mCalendarClient.getServiceName());
            } catch (GoogleLoginServiceBlockingHelper.AuthenticationException e) {
                Log.w(TAG, "Unable to update calendars from server -- could not "
                      + "authenticate user " + username, e);
                return;
            }
        } catch (GoogleLoginServiceNotFoundException e) {
            Log.e(TAG, "Could not find Google login service", e);
            return;
        } finally {
            if (loginHelper != null) {
                loginHelper.close();
            }
        }

        // get the current set of calendars.  we'll need to pay attention to
        // which calendars we get back from the server, so we can delete
        // calendars that have been deleted from the server.
        Set<Long> existingCalendarIds = new HashSet<Long>();

        final SQLiteDatabase db = getDatabase();
        db.beginTransaction();
        try {
            getCurrentCalendars(existingCalendarIds);

            // get and process the calendars meta feed
            GDataParser parser = null;
            try {
                String feedUrl = mCalendarClient.getUserCalendarsUrl(username);
                feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(username, feedUrl);
                parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
                // process the calendars
                processCalendars(username, parser, existingCalendarIds);
            } catch (ParseException pe) {
                Log.w(TAG, "Unable to process calendars from server -- could not "
                        + "parse calendar feed.", pe);
                return;
            } catch (IOException ioe) {
                Log.w(TAG, "Unable to process calendars from server -- encountered "
                        + "i/o error", ioe);
                return;
            } catch (HttpException e) {
                switch (e.getStatusCode()) {
                    case HttpException.SC_UNAUTHORIZED:
                        Log.w(TAG, "Unable to process calendars from server -- could not "
                                + "authenticate user.", e);
                        return;
                    case HttpException.SC_GONE:
                        Log.w(TAG, "Unable to process calendars from server -- encountered "
                                + "an AllDeletedUnavailableException, this should never happen", e);
                        return;
                    default:
                        Log.w(TAG, "Unable to process calendars from server -- error", e);
                        return;
                }
            } finally {
                if (parser != null) {
                    parser.close();
                }
            }

            // delete calendars that are no longer sent from the server.
            final Uri calendarContentUri = Calendars.CONTENT_URI;
            for (long calId : existingCalendarIds) {
                // NOTE: triggers delete all events, instances for this calendar.
                delete(ContentUris.withAppendedId(calendarContentUri, calId),
                        null /* where */, null /* selectionArgs */);
            }
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    
private voidfillBusyBits(int minDay, int startDay, int endDay, int startMinute, int endMinute, boolean allDay, int[] busybits, int[] allDayCounts)


        // The startDay can be less than the minDay if we have an event
        // that starts earlier than the time range we are interested in.
        // In that case, we ignore the time range that falls outside the
        // the range we are interested in.
        if (startDay < minDay) {
            startDay = minDay;
            startMinute = 0;
        }

        // Likewise, truncate the event's end day so that it doesn't go past
        // the expected range.
        int numDays = busybits.length;
        int stopDay = endDay;
        if (stopDay > minDay + numDays - 1) {
            stopDay = minDay + numDays - 1;
        }
        int dayIndex = startDay - minDay;

        if (allDay) {
            for (int day = startDay; day <= stopDay; day++, dayIndex++) {
                allDayCounts[dayIndex] += 1;
            }
            return;
        }

        for (int day = startDay; day <= stopDay; day++, dayIndex++) {
            int endTime = endMinute;
            // If the event ends on a future day, then show it extending to
            // the end of this day.
            if (endDay > day) {
                endTime = 24 * 60;
            }

            int startBit = startMinute / BUSYBIT_INTERVAL ;
            int endBit = (endTime + BUSYBIT_INTERVAL - 1) / BUSYBIT_INTERVAL;
            int len = endBit - startBit;
            if (len == 0) {
                len = 1;
            }
            if (len < 0 || len > 24) {
                Log.e("Cal", "fillBusyBits() error: len " + len
                        + " startMinute,endTime " + startMinute + " , " + endTime
                        + " startDay,endDay " + startDay + " , " + endDay);
            } else {
                int oneBits = BIT_MASKS[len];
                busybits[dayIndex] |= oneBits << startBit;
            }

            // Set the start minute to the beginning of the day, in
            // case this event spans multiple days.
            startMinute = 0;
        }
    
private android.app.AlarmManagergetAlarmManager()

        synchronized(mAlarmLock) {
            if (mAlarmManager == null) {
                Context context = getContext();
                if (context == null) {
                    Log.e(TAG, "getAlarmManager() cannot get Context");
                    return null;
                }
                Object service = context.getSystemService(Context.ALARM_SERVICE);
                mAlarmManager = (AlarmManager) service;
            }
            return mAlarmManager;
        }
    
private voidgetCurrentCalendars(java.util.Set calendarIds)

        Cursor cursor = query(Calendars.CONTENT_URI,
                new String[] { Calendars._ID },
                null /* selection */,
                null /* selectionArgs */,
                null /* sort */);
        if (cursor != null) {
            try {
                while (cursor.moveToNext()) {
                    calendarIds.add(cursor.getLong(0));
                }
            } finally {
                cursor.close();
            }
        }
    
protected java.lang.IterablegetMergers()

        return Collections.singletonList(new EventMerger());
    
public synchronized android.content.SyncAdaptergetSyncAdapter()

        if (mSyncAdapter == null) {
            mSyncAdapter = new CalendarSyncAdapter(getContext(), this);
        }
        return mSyncAdapter;
    
public java.lang.StringgetType(android.net.Uri url)

        int match = sURLMatcher.match(url);
        switch (match) {
        case EVENTS:
            return "vnd.android.cursor.dir/event";
        case EVENTS_ID:
            return "vnd.android.cursor.item/event";
        case REMINDERS:
            return "vnd.android.cursor.dir/reminder";
        case REMINDERS_ID:
            return "vnd.android.cursor.item/reminder";
        case CALENDAR_ALERTS:
            return "vnd.android.cursor.dir/calendar-alert";
        case CALENDAR_ALERTS_BY_INSTANCE:
            return "vnd.android.cursor.dir/calendar-alert-by-instance";
        case CALENDAR_ALERTS_ID:
            return "vnd.android.cursor.item/calendar-alert";
        case INSTANCES:
            return "vnd.android.cursor.dir/event-instance";
        case BUSYBITS:
            return "vnd.android.cursor.dir/busybits";
        default:
            throw new IllegalArgumentException("Unknown URL " + url);
        }
    
private android.database.CursorhandleBusyBitsQuery(android.database.sqlite.SQLiteQueryBuilder qb, int startDay, int endDay, java.lang.String[] projectionIn, java.lang.String selection, java.lang.String sort)

        final SQLiteDatabase db = getDatabase();
        acquireBusyBitRange(startDay, endDay);
        qb.setTables("BusyBits");
        qb.setProjectionMap(sBusyBitsProjectionMap);
        qb.appendWhere("day >= ");
        qb.appendWhere(String.valueOf(startDay));
        qb.appendWhere(" AND day <= ");
        qb.appendWhere(String.valueOf(endDay));
        return qb.query(db, projectionIn, selection, null, null, null, sort);
    
private android.database.CursorhandleInstanceQuery(android.database.sqlite.SQLiteQueryBuilder qb, long rangeBegin, long rangeEnd, java.lang.String[] projectionIn, java.lang.String selection, java.lang.String sort)

        final SQLiteDatabase db = getDatabase();
        // will lock the database.
        acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */);
        qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
                     "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
        qb.setProjectionMap(sInstancesProjectionMap);
        qb.appendWhere("begin <= ");
        qb.appendWhere(String.valueOf(rangeEnd));
        qb.appendWhere(" AND end >= ");
        qb.appendWhere(String.valueOf(rangeBegin));
        return qb.query(db, projectionIn, selection, null, null, null, sort);
    
private voidinsertBusyBitsLocked(long eventId, android.content.ContentValues values)
Updates the BusyBit table when a new event is inserted into the Events table. This is called after the event has been entered into the Events table. If the event time is not within the date range of the current BusyBits table, then the busy bits are not updated. The BusyBits table is not automatically expanded to include this event.

param
eventId the id of the newly created event
param
values the ContentValues for the new event

        MetaData.Fields fields = mMetaData.getFieldsLocked();
        if (fields.maxBusyBit == 0) {
            return;
        }

        // If this is a recurrence event, then the expanded Instances range
        // should be 0 because this is called after updateInstancesLocked().
        // But for now check this condition and report an error if it occurs.
        // In the future, we could even support recurring events by
        // expanding them here and updating the busy bits for each instance.
        if (isRecurrenceEvent(values))  {
            Log.e(TAG, "insertBusyBitsLocked(): unexpected recurrence event\n");
            return;
        }

        long dtstartMillis = values.getAsLong(Events.DTSTART);
        Long dtendMillis = values.getAsLong(Events.DTEND);
        if (dtendMillis == null) {
            dtendMillis = dtstartMillis;
        }

        boolean allDay = false;
        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
        if (allDayInteger != null) {
            allDay = allDayInteger != 0;
        }

        Time time = new Time();
        if (allDay) {
            time.timezone = Time.TIMEZONE_UTC;
        }

        ContentValues busyValues = new ContentValues();
        computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);

        int startDay = busyValues.getAsInteger(Instances.START_DAY);
        int endDay = busyValues.getAsInteger(Instances.END_DAY);

        // If the event time is not in the expanded BusyBits range,
        // then return.
        if (startDay > fields.maxBusyBit || endDay < fields.minBusyBit) {
            return;
        }

        // Allocate space for the busy bits, one 32-bit integer for each day,
        // plus 24 bytes for the count of events that occur in each time slot.
        int numDays = endDay - startDay + 1;
        int[] busybits = new int[numDays];
        int[] allDayCounts = new int[numDays];

        int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
        int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
        fillBusyBits(startDay, startDay, endDay, startMinute, endMinute,
                allDay, busybits, allDayCounts);
        mergeBusyBits(startDay, endDay, busybits, allDayCounts);
    
public android.net.UriinsertInternal(android.net.Uri url, android.content.ContentValues initialValues)

        final SQLiteDatabase db = getDatabase();
        long rowID;

        int match = sURLMatcher.match(url);
        switch (match) {
            case EVENTS:
                if (!isTemporary()) {
                    initialValues.put(Events._SYNC_DIRTY, 1);
                    if (!initialValues.containsKey(Events.DTSTART)) {
                        throw new RuntimeException("DTSTART field missing from event");
                    }
                }
                // TODO: avoid the call to updateBundleFromEvent if this is just finding local
                // changes.  or avoid for temp providers altogether, if we can compute this
                // during a merge.
                // TODO: do we really need to make a copy?
                ContentValues updatedValues = updateContentValuesFromEvent(initialValues);
                if (updatedValues == null) {
                    throw new RuntimeException("Could not insert event.");
                    // return null;
                }
                long rowId = mEventsInserter.insert(updatedValues);
                Uri uri = Uri.parse("content://" + url.getAuthority() + "/events/" + rowId);
                if (!isTemporary() && rowId != -1) {
                    updateEventRawTimesLocked(rowId, updatedValues);
                    updateInstancesLocked(updatedValues, rowId, true /* new event */, db);
                    insertBusyBitsLocked(rowId, updatedValues);
                    
                    // If we inserted a new event that specified the self-attendee
                    // status, then we need to add an entry to the attendees table.
                    if (initialValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
                        int status = initialValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
                        createAttendeeEntry(rowId, status);
                    }
                    triggerAppWidgetUpdate(rowId);
                }

                return uri;
            case CALENDARS:
                if (!isTemporary()) {
                    Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
                    if (syncEvents != null && syncEvents == 1) {
                        String account = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
                        String calendarUrl = initialValues.getAsString(Calendars.URL);
                        scheduleSync(account, false /* two-way sync */, calendarUrl);
                    }
                }
                rowID = mCalendarsInserter.insert(initialValues);
                return Uri.parse("content://calendar/calendars/" + rowID);
            case ATTENDEES:
                // currently, only sync may insert attendees.  we do not support
                // inserting or removing attendees (which can only be yourself)
                // from the app.
                // TODO: remove this restriction when we deal with the full
                // attendees feed.  we'll also need to put in some protection to
                // prevent updates to the attendees that the server might reject.
                if (!isTemporary()) {
                    throw new IllegalArgumentException("Can only insert attendees into "
                    + "the temporary provider.");
                }
                if (!initialValues.containsKey(Attendees.EVENT_ID)) {
                    throw new IllegalArgumentException("Attendees values must "
                        + "contain an event_id");
                }
                rowID = mAttendeesInserter.insert(initialValues);

                // Copy the attendee status value to the Events table.
                updateEventAttendeeStatus(db, initialValues);

                return Uri.parse("content://calendars/attendees/" + rowID);
            case REMINDERS:
                if (!initialValues.containsKey(Reminders.EVENT_ID)) {
                    throw new IllegalArgumentException("Reminders values must "
                        + "contain an event_id");
                }
                rowID = mRemindersInserter.insert(initialValues);

                if (!isTemporary()) {
                    // Schedule another event alarm, if necessary
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "insertInternal() changing reminder");
                    }
                    scheduleNextAlarm(false /* do not remove alarms */);
                }
                return Uri.parse("content://calendars/reminders/" + rowID);
            case CALENDAR_ALERTS:
                if (!initialValues.containsKey(CalendarAlerts.EVENT_ID)) {
                    throw new IllegalArgumentException("CalendarAlerts values must "
                        + "contain an event_id");
                }
                rowID = mCalendarAlertsInserter.insert(initialValues);

                return Uri.parse(CalendarAlerts.CONTENT_URI + "/" + rowID);
            case EXTENDED_PROPERTIES:
                if (!initialValues.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
                    throw new IllegalArgumentException("ExtendedProperties values must "
                        + "contain an event_id");
                }
                rowID = mExtendedPropertiesInserter.insert(initialValues);

                return Uri.parse("content://calendars/extendedproperties/" + rowID);
            case DELETED_EVENTS:
                if (isTemporary()) {
                    rowID = mDeletedEventsInserter.insert(initialValues);
                    return Uri.parse("content://calendar/deleted_events/" + rowID);
                }
                // fallthrough
            case EVENTS_ID:
            case REMINDERS_ID:
            case CALENDAR_ALERTS_ID:
            case EXTENDED_PROPERTIES_ID:
            case INSTANCES:
                throw new UnsupportedOperationException("Cannot insert into that URL");
            default:
                throw new IllegalArgumentException("Unknown URL " + url);
        }
    
public static booleanisRecurrenceEvent(android.content.ContentValues values)

        return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))||
                !TextUtils.isEmpty(values.getAsString(Events.RDATE))||
                !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)));
    
private voidmergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts)

        mDb.beginTransaction();
        try {
            mergeBusyBitsLocked(startDay, endDay, busybits, allDayCounts);
            mDb.setTransactionSuccessful();
        } finally {
            mDb.endTransaction();
        }
    
private voidmergeBusyBitsLocked(int startDay, int endDay, int[] busybits, int[] allDayCounts)

        final SQLiteDatabase db = getDatabase();
        Cursor cursor = null;
        try {
            String selection = "day>=" + startDay + " AND day<=" + endDay;
            cursor = db.query("BusyBits", sBusyBitProjection, selection, null, null, null, null);
            if (cursor == null) {
                return;
            }
            while (cursor.moveToNext()) {
                int day = cursor.getInt(BUSYBIT_INDEX_DAY);
                int busy = cursor.getInt(BUSYBIT_INDEX_BUSYBITS);
                int allDayCount = cursor.getInt(BUSYBIT_INDEX_ALL_DAY_COUNT);

                int dayIndex = day - startDay;
                busybits[dayIndex] |= busy;
                allDayCounts[dayIndex] += allDayCount;
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        // Allocate a map that we can reuse
        ContentValues values = new ContentValues();

        // Write the busy bits to the database
        int len = busybits.length;
        for (int dayIndex = 0; dayIndex < len; dayIndex++) {
            int busy = busybits[dayIndex];
            int allDayCount = allDayCounts[dayIndex];
            if (busy == 0 && allDayCount == 0) {
                continue;
            }
            int day = startDay + dayIndex;

            values.clear();
            values.put(BusyBits.DAY, day);
            values.put(BusyBits.BUSYBITS, busy);
            values.put(BusyBits.ALL_DAY_COUNT, allDayCount);
            db.replace("BusyBits", null, values);
        }
    
private voidmodifyCalendarSubscription(long id, boolean syncEvents)

        // get the account, url, and current selected state
        // for this calendar.
        Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
                new String[] { Calendars._SYNC_ACCOUNT,
                               Calendars.URL,
                               Calendars.SYNC_EVENTS},
                null /* selection */,
                null /* selectionArgs */,
                null /* sort */);

        String account = null;
        String calendarUrl = null;
        boolean oldSyncEvents = false;
        if (cursor != null) {
            try {
                cursor.moveToFirst();
                account = cursor.getString(0);
                calendarUrl = cursor.getString(1);
                oldSyncEvents = (cursor.getInt(2) != 0);
            } finally {
                cursor.close();
            }
        }

        if (TextUtils.isEmpty(account) || TextUtils.isEmpty(calendarUrl)) {
            // should not happen?
            Log.w(TAG, "Cannot update subscription because account "
            + "or calendar url empty -- should not happen.");
            return;
        }

        if (oldSyncEvents == syncEvents) {
            // nothing to do
            return;
        }

        // If we are no longer syncing a calendar then make sure that the
        // old calendar sync data is cleared.  Then if we later add this
        // calendar back, we will sync all the events.
        if (!syncEvents) {
            byte[] data = readSyncDataBytes(account);
            GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data);
            if (syncData != null) {
                syncData.feedData.remove(calendarUrl);
                data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData);
                writeSyncDataBytes(account, data);
            }

            // Delete all of the events in this calendar to save space.
            // This is the closest we can come to deleting a calendar.
            // Clients should never actually delete a calendar.  That won't
            // work.  We need to keep the calendar entry in the Calendars table
            // in order to know not to sync the events for that calendar from
            // the server.
            final SQLiteDatabase db = getDatabase();
            String[] args = new String[] {Long.toString(id)};
            db.delete("Events", CALENDAR_ID_SELECTION, args);
            // Note that we do not delete the matching entries
            // in the DeletedEvents table.  We will let those
            // deleted events propagate to the server.

            // TODO: there is a corner case to deal with here: namely, if
            // we edit or delete an event on the phone and then remove
            // (that is, stop syncing) a calendar, and if we also make a
            // change on the server to that event at about the same time,
            // then we will never propagate the changes from the phone to
            // the server.
        }

        // If the calendar is not selected for syncing, then don't download
        // events.
        scheduleSync(account, !syncEvents, calendarUrl);
    
protected voidonAccountsChanged(java.lang.String[] accountsArray)
Make sure that there are no entries for accounts that no longer exist. We are overriding this since we need to delete from the Calendars table, which is not syncable, which has triggers that will delete from the Events and DeletedEvents tables, which are syncable.

        super.onAccountsChanged(accountsArray);

        Map<String, Boolean> accounts = new HashMap<String, Boolean>();
        for (String account : accountsArray) {
            accounts.put(account, false);
        }

        mDb.beginTransaction();
        try {
            deleteRowsForRemovedAccounts(accounts, "Calendars",
                    SyncConstValue._SYNC_ACCOUNT);
            mDb.setTransactionSuccessful();
        } finally {
            mDb.endTransaction();
        }

        if (mCalendarClient == null) {
            return;
        }

        // If we have calendars for unknown accounts, delete them.
        // If there are no calendars at all for a given account, add the
        // default calendar.

        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
            entry.setValue(false);
            // TODO: remove this break when Calendar supports multiple accounts. Until then
            // pretend that only the first account exists.
            break;
        }

        Set<String> handledAccounts = Sets.newHashSet();
        ContentResolver cr = getContext().getContentResolver();
        if (Config.LOGV) Log.v(TAG, "querying calendars");
        Cursor c = cr.query(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
        try {
            while (c.moveToNext()) {
                String account = c.getString(0);
                if (handledAccounts.contains(account)) {
                    continue;
                }
                handledAccounts.add(account);
                if (accounts.containsKey(account)) {
                    if (Config.LOGV) {
                        Log.v(TAG, "calendars for account " + account
                                + " exist");
                    }
                    accounts.put(account, true /* hasCalendar */);
                }
            }
        } finally {
            c.close();
            c = null;
        }

        if (Config.LOGV) {
            Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
        }
        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
            String account = entry.getKey();
            boolean hasCalendar = entry.getValue();
            if (hasCalendar) {
                if (Config.LOGV) {
                    Log.v(TAG, "ignoring account " + account +
                         " since it matched an existing calendar");
                }
                continue;
            }
            String feedUrl = mCalendarClient.getDefaultCalendarUrl(account,
                    CalendarClient.PROJECTION_PRIVATE_SELF_ATTENDANCE, null/* query params */);
            feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
            if (Config.LOGV) {
                Log.v(TAG, "adding default calendar for account " + account);
            }
            ContentValues values = new ContentValues();
            values.put(Calendars._SYNC_ACCOUNT, account);
            values.put(Calendars.URL, feedUrl);
            values.put(Calendars.DISPLAY_NAME, "Default");
            values.put(Calendars.SYNC_EVENTS, 1);
            values.put(Calendars.SELECTED, 1);
            values.put(Calendars.HIDDEN, 0);
            values.put(Calendars.COLOR, -14069085 /* blue */);
            // this is just our best guess.  the real value will get updated
            // when the user does a sync.
            values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
            values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
            cr.insert(Calendars.CONTENT_URI, values);

            scheduleSync(account, false /* do a full sync */, null /* no url */);
        }
    
public booleanonCreate()

        super.onCreate();

        // Register for Intent broadcasts
        IntentFilter filter = new IntentFilter();

        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        final Context c = getContext();

        // We don't ever unregister this because this thread always wants
        // to receive notifications, even in the background.  And if this
        // thread is killed then the whole process will be killed and the
        // memory resources will be reclaimed.
        c.registerReceiver(mIntentReceiver, filter);

        mMetaData = new MetaData(mOpenHelper);
        updateTimezoneDependentFields();

        return true;
    
protected voidonDatabaseOpened(android.database.sqlite.SQLiteDatabase db)

        db.markTableSyncable("Events", "DeletedEvents");

        if (!isTemporary()) {
            mCalendarClient = new CalendarClient(
                new AndroidGDataClient(getContext(), CalendarSyncAdapter.USER_AGENT_APP_VERSION),
                new XmlCalendarGDataParserFactory(
                new AndroidXmlParserFactory()));
        }

        mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
        mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
        mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
        mDeletedEventsInserter = new DatabaseUtils.InsertHelper(db, "DeletedEvents");
        mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
        mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
        mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
        mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
        mExtendedPropertiesInserter =
            new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
    
public voidonSyncStop(android.content.SyncContext context, boolean success)

        super.onSyncStop(context, success);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onSyncStop() success: " + success);
        }
        scheduleNextAlarm(false /* do not remove alarms */);
        triggerAppWidgetUpdate(-1);
    
private voidprocessCalendars(java.lang.String username, com.google.wireless.gdata.parser.GDataParser parser, java.util.Set existingCalendarIds)

        CalendarsFeed feed = (CalendarsFeed) parser.init();
        Entry entry = null;
        ContentValues map = new ContentValues();
        final Uri calendarContentUri = Calendars.CONTENT_URI;
        while (parser.hasMoreData()) {
            entry = parser.readNextEntry(entry);
            if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
            CalendarEntry calendarEntry = (CalendarEntry) entry;
            String feedUrl = calendarEntryToContentValues(username, feed, calendarEntry, map);
            if (TextUtils.isEmpty(feedUrl)) {
                continue;
            }
            long calId = -1;

            Cursor c = query(calendarContentUri,
                    new String[] { Calendars._ID },
                    Calendars.URL + "='"
                            + feedUrl + '\'" /* selection */,
                    null /* selectionArgs */,
                    null /* sort */);
            if (c != null) {
                try {
                    if (c.moveToFirst()) {
                        calId = c.getLong(0);
                        existingCalendarIds.remove(calId);
                    }
                } finally {
                    c.close();
                }
            }

            if (calId != -1) {
                if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
                // don't override the existing "selected" or "hidden" settings.
                map.remove(Calendars.SELECTED);
                map.remove(Calendars.HIDDEN);
                // write to db directly, so we don't send a notification.
                updateInternal(ContentUris.withAppendedId(calendarContentUri, calId), map,
                       null /* where */, null /* selectionArgs */);
            } else {
                // Select this calendar for syncing and display if it is
                // selected and not hidden.
                int syncAndDisplay = 0;
                if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
                    syncAndDisplay = 1;
                }
                map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
                map.put(Calendars.SELECTED, syncAndDisplay);
                map.put(Calendars.HIDDEN, 0);
                map.put(Calendars._SYNC_ACCOUNT, username);
                if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
                // write to db directly, so we don't send a notification.
                Uri row = insertInternal(calendarContentUri, map);
            }
        }
    
public android.database.CursorqueryInternal(android.net.Uri url, java.lang.String[] projectionIn, java.lang.String selection, java.lang.String[] selectionArgs, java.lang.String sort)

        final SQLiteDatabase db = getDatabase();
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

        Cursor ret;

        // Generate the body of the query
        int match = sURLMatcher.match(url);
        switch (match)
        {
            case EVENTS:
                qb.setTables("Events, Calendars");
                qb.setProjectionMap(sEventsProjectionMap);
                qb.appendWhere("Events.calendar_id=Calendars._id");
                break;
            case EVENTS_ID:
                qb.setTables("Events, Calendars");
                qb.setProjectionMap(sEventsProjectionMap);
                qb.appendWhere("Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=");
                qb.appendWhere(url.getPathSegments().get(1));
                break;
            case DELETED_EVENTS:
                if (isTemporary()) {
                    qb.setTables("DeletedEvents");
                    break;
                } else {
                    throw new IllegalArgumentException("Unknown URL " + url);
                }
            case CALENDARS:
                qb.setTables("Calendars");
                // see if we want to update the list of calendars from the
                // server.
                String update = null;
                update = url.getQueryParameter("update");
                if ("1".equals(update)) {
                    fetchCalendarsFromServer();
                }

                break;
            case CALENDARS_ID:
                qb.setTables("Calendars");
                qb.appendWhere("_id=");
                qb.appendWhere(url.getPathSegments().get(1));
                break;
            case INSTANCES:
                long begin;
                long end;
                try {
                    begin = Long.valueOf(url.getPathSegments().get(2));
                } catch (NumberFormatException nfe) {
                    throw new IllegalArgumentException("Cannot parse begin "
                            + url.getPathSegments().get(2));
                }
                try {
                    end = Long.valueOf(url.getPathSegments().get(3));
                } catch (NumberFormatException nfe) {
                    throw new IllegalArgumentException("Cannot parse end "
                            + url.getPathSegments().get(3));
                }
                return handleInstanceQuery(qb, begin, end, projectionIn,
                                           selection, sort);
            case BUSYBITS:
                int startDay;
                int endDay;
                try {
                    startDay = Integer.valueOf(url.getPathSegments().get(2));
                } catch (NumberFormatException nfe) {
                    throw new IllegalArgumentException("Cannot parse start day "
                            + url.getPathSegments().get(2));
                }
                try {
                    endDay = Integer.valueOf(url.getPathSegments().get(3));
                } catch (NumberFormatException nfe) {
                    throw new IllegalArgumentException("Cannot parse end day "
                            + url.getPathSegments().get(3));
                }
                return handleBusyBitsQuery(qb, startDay, endDay, projectionIn,
                                           selection, sort);
            case ATTENDEES:
                qb.setTables("Attendees, Events, Calendars");
                qb.setProjectionMap(sAttendeesProjectionMap);
                qb.appendWhere("Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=Attendees.event_id");
                break;
            case ATTENDEES_ID:
                qb.setTables("Attendees, Events, Calendars");
                qb.setProjectionMap(sAttendeesProjectionMap);
                qb.appendWhere("Attendees._id=");
                qb.appendWhere(url.getPathSegments().get(1));
                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=Attendees.event_id");
                break;
            case REMINDERS:
                qb.setTables("Reminders");
                break;
            case REMINDERS_ID:
                qb.setTables("Reminders, Events, Calendars");
                qb.setProjectionMap(sRemindersProjectionMap);
                qb.appendWhere("Reminders._id=");
                qb.appendWhere(url.getLastPathSegment());
                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=Reminders.event_id");
                break;
            case CALENDAR_ALERTS:
                qb.setTables("CalendarAlerts, Events, Calendars");
                qb.setProjectionMap(sCalendarAlertsProjectionMap);
                qb.appendWhere("Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
                break;
            case CALENDAR_ALERTS_BY_INSTANCE:
                qb.setTables("CalendarAlerts, Events, Calendars");
                qb.setProjectionMap(sCalendarAlertsProjectionMap);
                qb.appendWhere("Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
                String groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
                return qb.query(db, projectionIn, selection, selectionArgs,
                        groupBy, null, sort);
            case CALENDAR_ALERTS_ID:
                qb.setTables("CalendarAlerts, Events, Calendars");
                qb.setProjectionMap(sCalendarAlertsProjectionMap);
                qb.appendWhere("CalendarAlerts._id=");
                qb.appendWhere(url.getLastPathSegment());
                qb.appendWhere(" AND Events.calendar_id=Calendars._id");
                qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
                break;
            case EXTENDED_PROPERTIES:
                qb.setTables("ExtendedProperties");
                break;
            case EXTENDED_PROPERTIES_ID:
                qb.setTables("ExtendedProperties, Events, Calendars");
                // not sure if we need a projection map or a join.  see what callers want.
//                qb.setProjectionMap(sExtendedPropertiesProjectionMap);
                qb.appendWhere("ExtendedProperties._id=");
                qb.appendWhere(url.getPathSegments().get(1));
//                qb.appendWhere(" AND Events.calendar_id = Calendars._id");
//                qb.appendWhere(" AND Events._id=ExtendedProperties.event_id");
                break;

            default:
                throw new IllegalArgumentException("Unknown URL " + url);
        }

        // run the query
        ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);

        return ret;
    
private com.android.providers.calendar.CalendarProvider$TimeRangereadEventStartEnd(long eventId)

        Cursor cursor = null;
        TimeRange range = new TimeRange();
        try {
            cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
                    new String[] { Events.DTSTART, Events.DTEND, Events.ALL_DAY },
                    null /* selection */,
                    null /* selectionArgs */,
                    null /* sort */);
            if (cursor == null) {
                return null;
            }
            cursor.moveToFirst();
            range.begin = cursor.getLong(0);
            range.end = cursor.getLong(1);
            range.allDay = cursor.getInt(2) != 0;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return range;
    
private voidremoveScheduledAlarmsLocked(android.database.sqlite.SQLiteDatabase db)
Removes the entries in the CalendarAlerts table for alarms that we have scheduled but that have not fired yet. We do this to ensure that we don't miss an alarm. The CalendarAlerts table keeps track of the alarms that we have scheduled but the actual alarm list is in memory and will be cleared if the phone reboots. We don't need to remove entries that have already fired, and in fact we should not remove them because we need to display the notifications until the user dismisses them. We could remove entries that have fired and been dismissed, but we leave them around for a while because it makes it easier to debug problems. Entries that are old enough will be cleaned up later when we schedule new alarms.

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "removing scheduled alarms");
        }
        db.delete(CalendarAlerts.TABLE_NAME,
                CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
    
private voidrescheduleMissedAlarms()

        AlarmManager manager = getAlarmManager();
        if (manager != null) {
            Context context = getContext();
            ContentResolver cr = context.getContentResolver();
            CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
        }
    
private voidrunScheduleNextAlarm(boolean removeAlarms)
This method runs in a background thread and schedules an alarm for the next calendar event, if necessary.

        // Do not schedule any alarms if this is a temporary database.
        if (isTemporary()) {
            return;
        }

        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        db.beginTransaction();
        try {
            if (removeAlarms) {
                removeScheduledAlarmsLocked(db);
            }
            scheduleNextAlarmLocked(db);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    
voidscheduleNextAlarm(boolean removeAlarms)

        Thread thread = new AlarmScheduler(removeAlarms);
        thread.start();
    
voidscheduleNextAlarmCheck(long triggerTime)

        AlarmManager manager = getAlarmManager();
        if (manager == null) {
            Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
            return;
        }
        Context context = getContext();
        Intent intent = new Intent(CalendarReceiver.SCHEDULE);
        intent.setClass(context, CalendarReceiver.class);
        PendingIntent pending = PendingIntent.getBroadcast(context,
                0, intent, PendingIntent.FLAG_NO_CREATE);
        if (pending != null) {
            // Cancel any previous alarms that do the same thing.
            manager.cancel(pending);
        }
        pending = PendingIntent.getBroadcast(context,
                0, intent, PendingIntent.FLAG_CANCEL_CURRENT);

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Time time = new Time();
            time.set(triggerTime);
            String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
            Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
        }

        manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
    
private voidscheduleNextAlarmLocked(android.database.sqlite.SQLiteDatabase db)
This method looks at the 24-hour window from now for any events that it needs to schedule. This method runs within a database transaction. It also runs in a background thread. The CalendarProvider keeps track of which alarms it has already scheduled to avoid scheduling them more than once and for debugging problems with alarms. It stores this knowledge in a database table called CalendarAlerts which persists across reboots. But the actual alarm list is in memory and disappears if the phone loses power. To avoid missing an alarm, we clear the entries in the CalendarAlerts table when we start up the CalendarProvider. Scheduling an alarm multiple times is not tragic -- we filter out the extra ones when we receive them. But we still need to keep track of the scheduled alarms. The main reason is that we need to prevent multiple notifications for the same alarm (on the receive side) in case we accidentally schedule the same alarm multiple times. We don't have visibility into the system's alarm list so we can never know for sure if we have already scheduled an alarm and it's better to err on scheduling an alarm twice rather than missing an alarm. Another reason we keep track of scheduled alarms in a database table is that it makes it easy to run an SQL query to find the next reminder that we haven't scheduled.

param
db the database

        AlarmManager alarmManager = getAlarmManager();
        if (alarmManager == null) {
            Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
            return;
        }

        final long currentMillis = System.currentTimeMillis();
        final long start = currentMillis - SCHEDULE_ALARM_SLACK;
        final long end = start + (24 * 60 * 60 * 1000);
        ContentResolver cr = getContext().getContentResolver();
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Time time = new Time();
            time.set(start);
            String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
            Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
        }

        // Clear old alarms but keep alarms around for a while to prevent
        // multiple alerts for the same reminder.  The "clearUpToTime'
        // should be further in the past than the point in time where
        // we start searching for events (the "start" variable defined above).
        long clearUpToTime = currentMillis - CLEAR_OLD_ALARM_THRESHOLD;
        db.delete("CalendarAlerts", CalendarAlerts.ALARM_TIME + "<" + clearUpToTime, null);

        long nextAlarmTime = end;
        long alarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
        if (alarmTime != -1 && alarmTime < nextAlarmTime) {
            nextAlarmTime = alarmTime;
        }

        // Extract events from the database sorted by alarm time.  The
        // alarm times are computed from Instances.begin (whose units
        // are milliseconds) and Reminders.minutes (whose units are
        // minutes).
        //
        // Also, ignore events whose end time is already in the past.
        // Also, ignore events alarms that we have already scheduled.
        //
        // Note 1: we can add support for the case where Reminders.minutes
        // equals -1 to mean use Calendars.minutes by adding a UNION for
        // that case where the two halves restrict the WHERE clause on
        // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
        //
        // Note 2: we have to name "myAlarmTime" different from the
        // "alarmTime" column in CalendarAlerts because otherwise the
        // query won't find multiple alarms for the same event.
        String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
            + " Instances.event_id AS eventId, begin, end,"
            + " title, allDay, method, minutes"
            + " FROM Instances INNER JOIN Events"
            + " ON (Events._id = Instances.event_id)"
            + " INNER JOIN Reminders"
            + " ON (Instances.event_id = Reminders.event_id)"
            + " WHERE method=" + Reminders.METHOD_ALERT
            + " AND myAlarmTime>=" + start
            + " AND myAlarmTime<=" + nextAlarmTime
            + " AND end>=" + currentMillis
            + " AND 0=(SELECT count(*) from CalendarAlerts CA"
            + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
            + " AND CA.alarmTime=myAlarmTime)"
            + " ORDER BY myAlarmTime,begin,title";

        acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */);
        Cursor cursor = null;
        try {
            cursor = db.rawQuery(query, null);

            int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
            int endIndex = cursor.getColumnIndex(Instances.END);
            int eventIdIndex = cursor.getColumnIndex("eventId");
            int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
            int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);

            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Time time = new Time();
                time.set(nextAlarmTime);
                String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                Log.d(TAG, "nextAlarmTime: " + alarmTimeStr
                        + " cursor results: " + cursor.getCount()
                        + " query: " + query);
            }

            while (cursor.moveToNext()) {
                // Schedule all alarms whose alarm time is as early as any
                // scheduled alarm.  For example, if the earliest alarm is at
                // 1pm, then we will schedule all alarms that occur at 1pm
                // but no alarms that occur later than 1pm.
                // Actually, we allow alarms up to a minute later to also
                // be scheduled so that we don't have to check immediately
                // again after an event alarm goes off.
                alarmTime = cursor.getLong(alarmTimeIndex);
                long eventId = cursor.getLong(eventIdIndex);
                int minutes = cursor.getInt(minutesIndex);
                long startTime = cursor.getLong(beginIndex);

                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    int titleIndex = cursor.getColumnIndex(Events.TITLE);
                    String title = cursor.getString(titleIndex);
                    Time time = new Time();
                    time.set(alarmTime);
                    String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
                    time.set(startTime);
                    String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                    long endTime = cursor.getLong(endIndex);
                    time.set(endTime);
                    String endTimeStr = time.format(" - %a, %b %d, %Y %I:%M%P");
                    time.set(currentMillis);
                    String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                    Log.d(TAG, "  looking at id: " + eventId + " " + title
                            + " " + startTime
                            + startTimeStr + endTimeStr + " alarm: "
                            + alarmTime + schedTime
                            + " currentTime: " + currentTimeStr);
                }

                if (alarmTime < nextAlarmTime) {
                    nextAlarmTime = alarmTime;
                } else if (alarmTime > nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS) {
                    // This event alarm (and all later ones) will be scheduled
                    // later.
                    break;
                }

                // Avoid an SQLiteContraintException by checking if this alarm
                // already exists in the table.
                if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        int titleIndex = cursor.getColumnIndex(Events.TITLE);
                        String title = cursor.getString(titleIndex);
                        Log.d(TAG, "  alarm exists for id: " + eventId + " " + title);
                    }
                    continue;
                }

                // Insert this alarm into the CalendarAlerts table
                long endTime = cursor.getLong(endIndex);
                Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
                        endTime, alarmTime, minutes);
                if (uri == null) {
                    Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed");
                    continue;
                }

                Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION);
                intent.setData(uri);

                // Also include the begin and end time of this event, because
                // we cannot determine that from the Events database table.
                intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, startTime);
                intent.putExtra(android.provider.Calendar.EVENT_END_TIME, endTime);
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    int titleIndex = cursor.getColumnIndex(Events.TITLE);
                    String title = cursor.getString(titleIndex);
                    Time time = new Time();
                    time.set(alarmTime);
                    String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
                    time.set(startTime);
                    String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                    time.set(endTime);
                    String endTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                    time.set(currentMillis);
                    String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
                    Log.d(TAG, "  scheduling " + title
                            + startTimeStr  + " - " + endTimeStr + " alarm: " + schedTime
                            + " currentTime: " + currentTimeStr
                            + " uri: " + uri);
                }
                PendingIntent sender = PendingIntent.getBroadcast(getContext(),
                        0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
                alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        // If we scheduled an event alarm, then schedule the next alarm check
        // for one minute past that alarm.  Otherwise, if there were no
        // event alarms scheduled, then check again in 24 hours.  If a new
        // event is inserted before the next alarm check, then this method
        // will be run again when the new event is inserted.
        if (nextAlarmTime != Long.MAX_VALUE) {
            scheduleNextAlarmCheck(nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS);
        } else {
            scheduleNextAlarmCheck(currentMillis + android.text.format.DateUtils.DAY_IN_MILLIS);
        }
    
private voidscheduleSync(java.lang.String account, boolean uploadChangesOnly, java.lang.String url)
Schedule a calendar sync for the account.

param
account the account for which to schedule a sync
param
uploadChangesOnly if set, specify that the sync should only send up local changes
param
url the url feed for the calendar to sync (may be null)

        Bundle extras = new Bundle();
        extras.putString(ContentResolver.SYNC_EXTRAS_ACCOUNT, account);
        extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
        if (url != null) {
            extras.putString("feed", url);
            extras.putBoolean(ContentResolver.SYNC_EXTRAS_FORCE, true);
        }
        getContext().getContentResolver().startSync(android.provider.Calendar.CONTENT_URI, extras);
    
private synchronized voidtriggerAppWidgetUpdate(long changedEventId)
Update any existing widgets with the changed events.

param
changedEventId Specific event known to be changed, otherwise -1. If present, we use it to decide if an update is necessary.

        Context context = getContext();
        if (context != null) {
            mAppWidgetProvider.providerUpdated(context, changedEventId);
        }
    
private voidupdateBusyBitsLocked(long eventId, android.content.ContentValues values)
Updates the busy bits for an event that is being updated. This is called before the event is updated in the Events table because we need to know the time of the event before it was changed.

param
eventId the id of the event being updated
param
values the ContentValues for the updated event

        MetaData.Fields fields = mMetaData.getFieldsLocked();
        if (fields.maxBusyBit == 0) {
            return;
        }

        // If this is a recurring event, then clear the BusyBits table.
        if (isRecurrenceEvent(values))  {
            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
                    0 /* startDay */, 0 /* endDay */);
            return;
        }

        // If the event fields being updated don't contain the start or end
        // time, then we don't need to bother updating the BusyBits table.
        Long dtstartLong = values.getAsLong(Events.DTSTART);
        Long dtendLong = values.getAsLong(Events.DTEND);
        if (dtstartLong == null && dtendLong == null) {
            return;
        }

        // If the timezone has changed, then clear the busy bits table
        // and return.
        String dbTimezone = fields.timezone;
        String localTimezone = TimeZone.getDefault().getID();
        boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
        if (timezoneChanged) {
            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
                    0 /* startDay */, 0 /* endDay */);
            return;
        }

        // Read the existing event start and end times from the Events table.
        TimeRange eventRange = readEventStartEnd(eventId);

        // Fill in the new start time (if missing) or the new end time (if
        // missing) from the existing event start and end times.
        long dtstartMillis;
        if (dtstartLong != null) {
            dtstartMillis = dtstartLong;
        } else {
            dtstartMillis = eventRange.begin;
        }

        long dtendMillis;
        if (dtendLong != null) {
            dtendMillis = dtendLong;
        } else {
            dtendMillis = eventRange.end;
        }

        // Compute the start and end Julian days for the event.
        Time time = new Time();
        if (eventRange.allDay) {
            time.timezone = Time.TIMEZONE_UTC;
        }
        ContentValues busyValues = new ContentValues();
        computeTimezoneDependentFields(eventRange.begin, eventRange.end, time, busyValues);
        int oldStartDay = busyValues.getAsInteger(Instances.START_DAY);
        int oldEndDay = busyValues.getAsInteger(Instances.END_DAY);

        boolean allDay = false;
        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
        if (allDayInteger != null) {
            allDay = allDayInteger != 0;
        }

        if (allDay) {
            time.timezone = Time.TIMEZONE_UTC;
        } else {
            time.timezone = TimeZone.getDefault().getID();
        }

        computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
        int newStartDay = busyValues.getAsInteger(Instances.START_DAY);
        int newEndDay = busyValues.getAsInteger(Instances.END_DAY);

        // If both the old and new event times are outside the expanded
        // BusyBits table, then return.
        if ((oldStartDay > fields.maxBusyBit || oldEndDay < fields.minBusyBit)
                && (newStartDay > fields.maxBusyBit || newEndDay < fields.minBusyBit)) {
            return;
        }

        // If the old event time is within the expanded Instances range,
        // then clear the BusyBits table and return.
        if (oldStartDay <= fields.maxBusyBit && oldEndDay >= fields.minBusyBit) {
            // We could recompute the busy bits for the days containing the
            // old event time.  For now, just clear the BusyBits table.
            mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
                    0 /* startDay */, 0 /* endDay */);
            return;
        }

        // The new event time is within the expanded Instances range.
        // So insert the busy bits for that day (or days).

        // Allocate space for the busy bits, one 32-bit integer for each day,
        // plus 24 bytes for the count of events that occur in each time slot.
        int numDays = newEndDay - newStartDay + 1;
        int[] busybits = new int[numDays];
        int[] allDayCounts = new int[numDays];

        int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
        int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
        fillBusyBits(newStartDay, newStartDay, newEndDay, startMinute, endMinute,
                allDay, busybits, allDayCounts);
        mergeBusyBits(newStartDay, newEndDay, busybits, allDayCounts);
    
private android.content.ContentValuesupdateContentValuesFromEvent(android.content.ContentValues initialValues)

        try {
            ContentValues values = new ContentValues(initialValues);

            long last = calculateLastDate(values);
            if (last != -1) {
                values.put(Events.LAST_DATE, last);
            }

            return values;
        } catch (DateException e) {
            // don't add it if there was an error
            Log.w(TAG, "Could not calculate last date.", e);
            return null;
        }
    
private voidupdateEventAttendeeStatus(android.database.sqlite.SQLiteDatabase db, android.content.ContentValues attendeeValues)
Updates the attendee status in the Events table to be consistent with the value in the Attendees table.

param
db the database
param
attendeeValues the column values for one row in the Attendees table.

        // Get the event id for this attendee
        long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);

        // Currently, we only fetch the attendee for the owner of the calendar
        // so all the following expensive code is just wasted overhead.
        // When we actually support multiple attendees for an event, we will
        // have to execute this code (and perhaps tune it to make it as
        // efficient as possible).
        if (MULTIPLE_ATTENDEES_PER_EVENT) {
            // Get the calendar id for this event
            Cursor cursor = null;
            long calId;
            try {
                cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
                        new String[] { Events.CALENDAR_ID },
                        null /* selection */,
                        null /* selectionArgs */,
                        null /* sort */);
                if (cursor == null) {
                    return;
                }
                cursor.moveToFirst();
                calId = cursor.getLong(0);
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }

            // Get the feed for this Calendar
            String calendarUrl = null;
            cursor = null;
            try {
                cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
                        new String[] { Calendars.URL },
                        null /* selection */,
                        null /* selectionArgs */,
                        null /* sort */);
                if (cursor == null) {
                    return;
                }
                cursor.moveToFirst();
                calendarUrl = cursor.getString(0);
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }

            // Get the email address from the calendar feed
            String calendarEmail = calendarEmailAddressFromFeedUrl(calendarUrl);
            if (calendarEmail == null) {
                return;
            }

            // Get the email address for this attendee
            String attendeeEmail = null;
            if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
                attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
            }

            // If the attendee email does not match the calendar email, then this
            // attendee is not the owner of this calendar so we don't update the
            // selfAttendeeStatus in the event.
            if (!calendarEmail.equals(attendeeEmail)) {
                return;
            }
        }

        int status = Attendees.ATTENDEE_STATUS_NONE;
        if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
            int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
            if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
                status = Attendees.ATTENDEE_STATUS_ACCEPTED;
            }
        }

        if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
            status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
        }

        ContentValues values = new ContentValues();
        values.put(Events.SELF_ATTENDEE_STATUS, status);
        db.update("Events", values, "_id="+eventId, null);
    
private voidupdateEventRawTimesLocked(long eventId, android.content.ContentValues values)

        ContentValues rawValues = new ContentValues();

        rawValues.put("event_id", eventId);

        String timezone = values.getAsString(Events.EVENT_TIMEZONE);

        boolean allDay = false;
        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
        if (allDayInteger != null) {
            allDay = allDayInteger != 0;
        }

        if (allDay || TextUtils.isEmpty(timezone)) {
            // floating timezone
            timezone = Time.TIMEZONE_UTC;
        }

        Time time = new Time(timezone);
        time.allDay = allDay;
        Long dtstartMillis = values.getAsLong(Events.DTSTART);
        if (dtstartMillis != null) {
            time.set(dtstartMillis);
            rawValues.put("dtstart2445", time.format2445());
        }

        Long dtendMillis = values.getAsLong(Events.DTEND);
        if (dtendMillis != null) {
            time.set(dtendMillis);
            rawValues.put("dtend2445", time.format2445());
        }

        Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
        if (originalInstanceMillis != null) {
            // This is a recurrence exception so we need to get the all-day
            // status of the original recurring event in order to format the
            // date correctly.
            allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
            if (allDayInteger != null) {
                time.allDay = allDayInteger != 0;
            }
            time.set(originalInstanceMillis);
            rawValues.put("originalInstanceTime2445", time.format2445());
        }

        Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
        if (lastDateMillis != null) {
            time.allDay = allDay;
            time.set(lastDateMillis);
            rawValues.put("lastDate2445", time.format2445());
        }

        mEventsRawTimesInserter.replace(rawValues);
    
private voidupdateInstancesLocked(android.content.ContentValues values, long rowId, boolean newEvent, android.database.sqlite.SQLiteDatabase db)

        if (isRecurrenceEvent(values))  {
            // TODO: insert the new recurrence into the instances table.
            mMetaData.clearInstanceRange();
            return;
        }

        // If there are no expanded Instances, then return.
        MetaData.Fields fields = mMetaData.getFieldsLocked();
        if (fields.maxInstance == 0) {
            return;
        }

        // if the event is in the expanded range, insert
        // into the instances table.
        // TODO: deal with durations.  currently, durations are only used in
        // recurrences.

        Long dtstartMillis = values.getAsLong(Events.DTSTART);

        if (dtstartMillis == null) {
            if (newEvent) {
                // must be present for a new event.
                throw new RuntimeException("DTSTART missing.");
            }
            if (Config.LOGV) Log.v(TAG, "Missing DTSTART.  "
                    + "No need to update instance.");
            return;
        }

        if (!newEvent) {
            db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */);
        }

        Long dtendMillis = values.getAsLong(Events.DTEND);
        if (dtendMillis == null) {
            dtendMillis = dtstartMillis;
        }

        if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
            ContentValues instanceValues = new ContentValues();
            instanceValues.put(Instances.EVENT_ID, rowId);
            instanceValues.put(Instances.BEGIN, dtstartMillis);
            instanceValues.put(Instances.END, dtendMillis);

            boolean allDay = false;
            Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
            if (allDayInteger != null) {
                allDay = allDayInteger != 0;
            }

            // Update the timezone-dependent fields.
            Time local = new Time();
            if (allDay) {
                local.timezone = Time.TIMEZONE_UTC;
            } else {
                local.timezone = fields.timezone;
            }

            computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
            mInstancesInserter.insert(instanceValues);
        }
    
public intupdateInternal(android.net.Uri url, android.content.ContentValues values, java.lang.String where, java.lang.String[] selectionArgs)

        // TODO: remove this restriction
        if (!TextUtils.isEmpty(where)) {
            throw new IllegalArgumentException(
                    "WHERE based updates not supported");
        }
        final SQLiteDatabase db = getDatabase();

        int match = sURLMatcher.match(url);
        switch (match) {
            case CALENDARS_ID:
            {
                long id = ContentUris.parseId(url);
                Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
                if (syncEvents != null && !isTemporary()) {
                    modifyCalendarSubscription(id, syncEvents == 1);
                }

                int result = db.update("Calendars", values, "_id="+ id, null);
                if (!isTemporary()) {
                    // When we change the display status of a Calendar
                    // we need to update the busy bits.
                    if (values.containsKey(Calendars.SELECTED) || (syncEvents != null)) {
                        // Clear the BusyBits table.
                        mMetaData.clearBusyBitRange();
                    }
                }

                return result;
            }
            case EVENTS_ID:
            {
                long id = ContentUris.parseId(url);
                if (!isTemporary()) {
                    values.put(Events._SYNC_DIRTY, 1);

                    // Disallow updating the attendee status in the Events
                    // table.  In the future, we could support this but we
                    // would have to query and update the attendees table
                    // to keep the values consistent.
                    if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
                        throw new IllegalArgumentException("Updating "
                                + Events.SELF_ATTENDEE_STATUS
                                + " in Events table is not allowed.");
                    }

                    if (values.containsKey(Events.HTML_URI)) {
                        throw new IllegalArgumentException("Updating "
                                + Events.HTML_URI
                                + " in Events table is not allowed.");
                    }

                    updateBusyBitsLocked(id, values);
                }

                ContentValues updatedValues = updateContentValuesFromEvent(values);
                if (updatedValues == null) {
                    Log.w(TAG, "Could not update event.");
                    return 0;
                }

                int result = db.update("Events", updatedValues, "_id="+id, null);
                if (!isTemporary()) {
                    if (result > 0) {
                        updateEventRawTimesLocked(id, updatedValues);
                        updateInstancesLocked(updatedValues, id, false /* not a new event */, db);

                        if (values.containsKey(Events.DTSTART)) {
                            // The start time of the event changed, so run the
                            // event alarm scheduler.
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "updateInternal() changing event");
                            }
                            scheduleNextAlarm(false /* do not remove alarms */);
                            triggerAppWidgetUpdate(id);
                        }
                    }
                }
                return result;
            }
            case ATTENDEES_ID:
            {
                // Copy the attendee status value to the Events table.
                updateEventAttendeeStatus(db, values);

                long id = ContentUris.parseId(url);
                return db.update("Attendees", values, "_id="+id, null);
            }
            case CALENDAR_ALERTS_ID:
            {
                long id = ContentUris.parseId(url);
                return db.update("CalendarAlerts", values, "_id="+id, null);
            }
            case REMINDERS_ID:
            {
                long id = ContentUris.parseId(url);
                int result = db.update("Reminders", values, "_id="+id, null);
                if (!isTemporary()) {
                    // Reschedule the event alarms because the
                    // "minutes" field may have changed.
                    if (Log.isLoggable(TAG, Log.DEBUG)) {
                        Log.d(TAG, "updateInternal() changing reminder");
                    }
                    scheduleNextAlarm(false /* do not remove alarms */);
                }
                return result;
            }
            case EXTENDED_PROPERTIES_ID:
            {
                long id = ContentUris.parseId(url);
                return db.update("ExtendedProperties", values, "_id="+id, null);
            }
            default:
                throw new IllegalArgumentException("Unknown URL " + url);
        }
    
private voidupdateTimezoneDependentFields()
This creates a background thread to check the timezone and update the timezone dependent fields in the Instances table if the timezone has changes.

        Thread thread = new TimezoneCheckerThread();
        thread.start();
    
protected booleanupgradeDatabase(android.database.sqlite.SQLiteDatabase db, int oldVersion, int newVersion)

        Log.i(TAG, "Upgrading DB from version " + oldVersion
                    + " to " + newVersion);
        if (oldVersion < 46) {
            dropTables(db);
            bootstrapDatabase(db);
            return false; // this was lossy
        }

        if (oldVersion == 46) {
            Log.w(TAG, "Upgrading CalendarAlerts table");
            db.execSQL("UPDATE CalendarAlerts SET reminder_id=NULL;");
            db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN minutes INTEGER DEFAULT 0;");
            oldVersion += 1;
        }

        if (oldVersion == 47) {
            // Changing to version 48 was intended to force a data wipe
            dropTables(db);
            bootstrapDatabase(db);
            return false; // this was lossy
        }

        if (oldVersion == 48) {
            // Changing to version 49 was intended to force a data wipe
            dropTables(db);
            bootstrapDatabase(db);
            return false; // this was lossy
        }

        if (oldVersion == 49) {
            Log.w(TAG, "Upgrading DeletedEvents table");

            // We don't have enough information to fill in the correct
            // value of the calendar_id for old rows in the DeletedEvents
            // table, but rows in that table are transient so it is unlikely
            // that there are any rows.  Plus, the calendar_id is used only
            // when deleting a calendar, which is a rare event.  All new rows
            // will have the correct calendar_id.
            db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");

            // Trigger to remove a calendar's events when we delete the calendar
            db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
            db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
                        "BEGIN " +
                            "DELETE FROM Events WHERE calendar_id = old._id;" +
                            "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
                        "END");
            db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
            oldVersion += 1;
        }

        if (oldVersion == 50) {
            // This should have been deleted in the upgrade from version 49
            // but we missed it.
            db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
            oldVersion += 1;
        }

        if (oldVersion == 51) {
            // We added "originalAllDay" to the Events table to keep track of
            // the allDay status of the original recurring event for entries
            // that are exceptions to that recurring event.  We need this so
            // that we can format the date correctly for the "originalInstanceTime"
            // column when we make a change to the recurrence exception and
            // send it to the server.
            db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");

            // Iterate through the Events table and for each recurrence
            // exception, fill in the correct value for "originalAllDay",
            // if possible.  The only times where this might not be possible
            // are (1) the original recurring event no longer exists, or
            // (2) the original recurring event does not yet have a _sync_id
            // because it was created on the phone and hasn't been synced to the
            // server yet.  In both cases the originalAllDay field will be set
            // to null.  In the first case we don't care because the recurrence
            // exception will not be displayed and we won't be able to make
            // any changes to it (and even if we did, the server should ignore
            // them, right?).  In the second case, the calendar client already
            // disallows making changes to an instance of a recurring event
            // until the recurring event has been synced to the server so the
            // second case should never occur.

            // "cursor" iterates over all the recurrences exceptions.
            Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
                    + " WHERE originalEvent IS NOT NULL", null /* selection args */);
            if (cursor != null) {
                try {
                    while (cursor.moveToNext()) {
                        long id = cursor.getLong(0);
                        String originalEvent = cursor.getString(1);

                        // Find the original recurring event (if it exists)
                        Cursor recur = db.rawQuery("SELECT allDay FROM Events"
                                + " WHERE _sync_id=?", new String[] {originalEvent});
                        if (recur == null) {
                            continue;
                        }

                        try {
                            // Fill in the "originalAllDay" field of the
                            // recurrence exception with the "allDay" value
                            // from the recurring event.
                            if (recur.moveToNext()) {
                                int allDay = recur.getInt(0);
                                db.execSQL("UPDATE Events SET originalAllDay=" + allDay
                                        + " WHERE _id="+id);
                            }
                        } finally {
                            recur.close();
                        }
                    }
                } finally {
                    cursor.close();
                }
            }
            oldVersion += 1;
        }

        if (oldVersion == 52) {
            Log.w(TAG, "Upgrading CalendarAlerts table");
            db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
            db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
            db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
            oldVersion += 1;
        }

        if (oldVersion == 53) {
            Log.w(TAG, "adding eventSyncAccountAndIdIndex");
            db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
                    + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");
            oldVersion += 1;
        }

        return true; // this was lossless