Home | History | Annotate | Download | only in calendar
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.providers.calendar;
     18 
     19 import com.android.calendarcommon2.DateException;
     20 import com.android.calendarcommon2.Duration;
     21 import com.android.calendarcommon2.EventRecurrence;
     22 import com.android.calendarcommon2.RecurrenceProcessor;
     23 import com.android.calendarcommon2.RecurrenceSet;
     24 import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
     25 
     26 import android.content.ContentValues;
     27 import android.database.Cursor;
     28 import android.database.DatabaseUtils;
     29 import android.database.sqlite.SQLiteDatabase;
     30 import android.database.sqlite.SQLiteQueryBuilder;
     31 import android.os.Debug;
     32 import android.provider.CalendarContract.Calendars;
     33 import android.provider.CalendarContract.Events;
     34 import android.provider.CalendarContract.Instances;
     35 import android.text.TextUtils;
     36 import android.text.format.Time;
     37 import android.util.Log;
     38 import android.util.TimeFormatException;
     39 
     40 import java.util.ArrayList;
     41 import java.util.HashMap;
     42 import java.util.Set;
     43 
     44 public class CalendarInstancesHelper {
     45     public static final class EventInstancesMap extends
     46             HashMap<String, CalendarInstancesHelper.InstancesList> {
     47         public void add(String syncIdKey, ContentValues values) {
     48             CalendarInstancesHelper.InstancesList instances = get(syncIdKey);
     49             if (instances == null) {
     50                 instances = new CalendarInstancesHelper.InstancesList();
     51                 put(syncIdKey, instances);
     52             }
     53             instances.add(values);
     54         }
     55     }
     56 
     57     public static final class InstancesList extends ArrayList<ContentValues> {
     58     }
     59 
     60     private static final String TAG = "CalInstances";
     61     private final CalendarDatabaseHelper mDbHelper;
     62     private final MetaData mMetaData;
     63     private final CalendarCache mCalendarCache;
     64 
     65     private static final String SQL_WHERE_GET_EVENTS_ENTRIES =
     66             "((" + Events.DTSTART + " <= ? AND "
     67                     + "(" + Events.LAST_DATE + " IS NULL OR " + Events.LAST_DATE + " >= ?)) OR "
     68             + "(" + Events.ORIGINAL_INSTANCE_TIME + " IS NOT NULL AND "
     69                     + Events.ORIGINAL_INSTANCE_TIME
     70                     + " <= ? AND " + Events.ORIGINAL_INSTANCE_TIME + " >= ?)) AND "
     71             + "(" + Calendars.SYNC_EVENTS + " != ?) AND "
     72             + "(" + Events.LAST_SYNCED + " = ?)";
     73 
     74     /**
     75      * Determines the set of Events where the _id matches the first query argument, or the
     76      * originalId matches the second argument.  Returns the _id field from the set of
     77      * Instances whose event_id field matches one of those events.
     78      */
     79     private static final String SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED =
     80             Instances._ID + " IN " +
     81             "(SELECT " + Tables.INSTANCES + "." + Instances._ID + " as _id" +
     82             " FROM " + Tables.INSTANCES +
     83             " INNER JOIN " + Tables.EVENTS +
     84             " ON (" +
     85             Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID +
     86             ")" +
     87             " WHERE " + Tables.EVENTS + "." + Events._ID + "=? OR " +
     88                     Tables.EVENTS + "." + Events.ORIGINAL_ID + "=?)";
     89 
     90     /**
     91      * Determines the set of Events where the _sync_id matches the first query argument, or the
     92      * originalSyncId matches the second argument.  Returns the _id field from the set of
     93      * Instances whose event_id field matches one of those events.
     94      */
     95     private static final String SQL_WHERE_ID_FROM_INSTANCES_SYNCED =
     96             Instances._ID + " IN " +
     97             "(SELECT " + Tables.INSTANCES + "." + Instances._ID + " as _id" +
     98             " FROM " + Tables.INSTANCES +
     99             " INNER JOIN " + Tables.EVENTS +
    100             " ON (" +
    101             Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID +
    102             ")" +
    103             " WHERE " + Tables.EVENTS + "." + Events._SYNC_ID + "=?" + " OR " +
    104                     Tables.EVENTS + "." + Events.ORIGINAL_SYNC_ID + "=?)";
    105 
    106     private static final String[] EXPAND_COLUMNS = new String[] {
    107             Events._ID,
    108             Events._SYNC_ID,
    109             Events.STATUS,
    110             Events.DTSTART,
    111             Events.DTEND,
    112             Events.EVENT_TIMEZONE,
    113             Events.RRULE,
    114             Events.RDATE,
    115             Events.EXRULE,
    116             Events.EXDATE,
    117             Events.DURATION,
    118             Events.ALL_DAY,
    119             Events.ORIGINAL_SYNC_ID,
    120             Events.ORIGINAL_INSTANCE_TIME,
    121             Events.CALENDAR_ID,
    122             Events.DELETED
    123     };
    124 
    125     // To determine if a recurrence exception originally overlapped the
    126     // window, we need to assume a maximum duration, since we only know
    127     // the original start time.
    128     private static final int MAX_ASSUMED_DURATION = 7 * 24 * 60 * 60 * 1000;
    129 
    130     public CalendarInstancesHelper(CalendarDatabaseHelper calendarDbHelper, MetaData metaData) {
    131         mDbHelper = calendarDbHelper;
    132         mMetaData = metaData;
    133         mCalendarCache = new CalendarCache(mDbHelper);
    134     }
    135 
    136     /**
    137      * Extract the value from the specifed row and column of the Events table.
    138      *
    139      * @param db The database to access.
    140      * @param rowId The Event's _id.
    141      * @param columnName The name of the column to access.
    142      * @return The value in string form.
    143      */
    144     private static String getEventValue(SQLiteDatabase db, long rowId, String columnName) {
    145         String where = "SELECT " + columnName + " FROM " + Tables.EVENTS +
    146             " WHERE " + Events._ID + "=?";
    147         return DatabaseUtils.stringForQuery(db, where,
    148                 new String[] { String.valueOf(rowId) });
    149     }
    150 
    151     /**
    152      * Perform instance expansion on the given entries.
    153      *
    154      * @param begin Window start (ms).
    155      * @param end Window end (ms).
    156      * @param localTimezone
    157      * @param entries The entries to process.
    158      */
    159     protected void performInstanceExpansion(long begin, long end, String localTimezone,
    160             Cursor entries) {
    161         // TODO: this only knows how to work with events that have been synced with the server
    162         RecurrenceProcessor rp = new RecurrenceProcessor();
    163 
    164         // Key into the instance values to hold the original event concatenated
    165         // with calendar id.
    166         final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR";
    167 
    168         int statusColumn = entries.getColumnIndex(Events.STATUS);
    169         int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
    170         int dtendColumn = entries.getColumnIndex(Events.DTEND);
    171         int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
    172         int durationColumn = entries.getColumnIndex(Events.DURATION);
    173         int rruleColumn = entries.getColumnIndex(Events.RRULE);
    174         int rdateColumn = entries.getColumnIndex(Events.RDATE);
    175         int exruleColumn = entries.getColumnIndex(Events.EXRULE);
    176         int exdateColumn = entries.getColumnIndex(Events.EXDATE);
    177         int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
    178         int idColumn = entries.getColumnIndex(Events._ID);
    179         int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
    180         int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_SYNC_ID);
    181         int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
    182         int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID);
    183         int deletedColumn = entries.getColumnIndex(Events.DELETED);
    184 
    185         ContentValues initialValues;
    186         CalendarInstancesHelper.EventInstancesMap instancesMap =
    187             new CalendarInstancesHelper.EventInstancesMap();
    188 
    189         Duration duration = new Duration();
    190         Time eventTime = new Time();
    191 
    192         // Invariant: entries contains all events that affect the current
    193         // window.  It consists of:
    194         // a) Individual events that fall in the window.  These will be
    195         //    displayed.
    196         // b) Recurrences that included the window.  These will be displayed
    197         //    if not canceled.
    198         // c) Recurrence exceptions that fall in the window.  These will be
    199         //    displayed if not cancellations.
    200         // d) Recurrence exceptions that modify an instance inside the
    201         //    window (subject to 1 week assumption above), but are outside
    202         //    the window.  These will not be displayed.  Cases c and d are
    203         //    distinguished by the start / end time.
    204 
    205         while (entries.moveToNext()) {
    206             try {
    207                 initialValues = null;
    208 
    209                 boolean allDay = entries.getInt(allDayColumn) != 0;
    210 
    211                 String eventTimezone = entries.getString(eventTimezoneColumn);
    212                 if (allDay || TextUtils.isEmpty(eventTimezone)) {
    213                     // in the events table, allDay events start at midnight.
    214                     // this forces them to stay at midnight for all day events
    215                     // TODO: check that this actually does the right thing.
    216                     eventTimezone = Time.TIMEZONE_UTC;
    217                 }
    218 
    219                 long dtstartMillis = entries.getLong(dtstartColumn);
    220                 Long eventId = Long.valueOf(entries.getLong(idColumn));
    221 
    222                 String durationStr = entries.getString(durationColumn);
    223                 if (durationStr != null) {
    224                     try {
    225                         duration.parse(durationStr);
    226                     }
    227                     catch (DateException e) {
    228                         if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    229                             Log.w(CalendarProvider2.TAG, "error parsing duration for event "
    230                                     + eventId + "'" + durationStr + "'", e);
    231                         }
    232                         duration.sign = 1;
    233                         duration.weeks = 0;
    234                         duration.days = 0;
    235                         duration.hours = 0;
    236                         duration.minutes = 0;
    237                         duration.seconds = 0;
    238                         durationStr = "+P0S";
    239                     }
    240                 }
    241 
    242                 String syncId = entries.getString(syncIdColumn);
    243                 String originalEvent = entries.getString(originalEventColumn);
    244 
    245                 long originalInstanceTimeMillis = -1;
    246                 if (!entries.isNull(originalInstanceTimeColumn)) {
    247                     originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
    248                 }
    249                 int status = entries.getInt(statusColumn);
    250                 boolean deleted = (entries.getInt(deletedColumn) != 0);
    251 
    252                 String rruleStr = entries.getString(rruleColumn);
    253                 String rdateStr = entries.getString(rdateColumn);
    254                 String exruleStr = entries.getString(exruleColumn);
    255                 String exdateStr = entries.getString(exdateColumn);
    256                 long calendarId = entries.getLong(calendarIdColumn);
    257                 // key into instancesMap
    258                 String syncIdKey = CalendarInstancesHelper.getSyncIdKey(syncId, calendarId);
    259 
    260                 RecurrenceSet recur = null;
    261                 try {
    262                     recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
    263                 } catch (EventRecurrence.InvalidFormatException e) {
    264                     if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    265                         Log.w(CalendarProvider2.TAG, "Could not parse RRULE recurrence string: "
    266                                 + rruleStr, e);
    267                     }
    268                     continue;
    269                 }
    270 
    271                 if (null != recur && recur.hasRecurrence()) {
    272                     // the event is repeating
    273 
    274                     if (status == Events.STATUS_CANCELED) {
    275                         // should not happen!
    276                         if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    277                             Log.e(CalendarProvider2.TAG, "Found canceled recurring event in "
    278                                     + "Events table.  Ignoring.");
    279                         }
    280                         continue;
    281                     }
    282                     if (deleted) {
    283                         if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
    284                             Log.d(CalendarProvider2.TAG, "Found deleted recurring event in "
    285                                     + "Events table.  Ignoring.");
    286                         }
    287                         continue;
    288                     }
    289 
    290                     // need to parse the event into a local calendar.
    291                     eventTime.timezone = eventTimezone;
    292                     eventTime.set(dtstartMillis);
    293                     eventTime.allDay = allDay;
    294 
    295                     if (durationStr == null) {
    296                         // should not happen.
    297                         if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    298                             Log.e(CalendarProvider2.TAG, "Repeating event has no duration -- "
    299                                     + "should not happen.");
    300                         }
    301                         if (allDay) {
    302                             // set to one day.
    303                             duration.sign = 1;
    304                             duration.weeks = 0;
    305                             duration.days = 1;
    306                             duration.hours = 0;
    307                             duration.minutes = 0;
    308                             duration.seconds = 0;
    309                             durationStr = "+P1D";
    310                         } else {
    311                             // compute the duration from dtend, if we can.
    312                             // otherwise, use 0s.
    313                             duration.sign = 1;
    314                             duration.weeks = 0;
    315                             duration.days = 0;
    316                             duration.hours = 0;
    317                             duration.minutes = 0;
    318                             if (!entries.isNull(dtendColumn)) {
    319                                 long dtendMillis = entries.getLong(dtendColumn);
    320                                 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
    321                                 durationStr = "+P" + duration.seconds + "S";
    322                             } else {
    323                                 duration.seconds = 0;
    324                                 durationStr = "+P0S";
    325                             }
    326                         }
    327                     }
    328 
    329                     long[] dates;
    330                     dates = rp.expand(eventTime, recur, begin, end);
    331 
    332                     // Initialize the "eventTime" timezone outside the loop.
    333                     // This is used in computeTimezoneDependentFields().
    334                     if (allDay) {
    335                         eventTime.timezone = Time.TIMEZONE_UTC;
    336                     } else {
    337                         eventTime.timezone = localTimezone;
    338                     }
    339 
    340                     long durationMillis = duration.getMillis();
    341                     for (long date : dates) {
    342                         initialValues = new ContentValues();
    343                         initialValues.put(Instances.EVENT_ID, eventId);
    344 
    345                         initialValues.put(Instances.BEGIN, date);
    346                         long dtendMillis = date + durationMillis;
    347                         initialValues.put(Instances.END, dtendMillis);
    348 
    349                         CalendarInstancesHelper.computeTimezoneDependentFields(date, dtendMillis,
    350                                 eventTime, initialValues);
    351                         instancesMap.add(syncIdKey, initialValues);
    352                     }
    353                 } else {
    354                     // the event is not repeating
    355                     initialValues = new ContentValues();
    356 
    357                     // if this event has an "original" field, then record
    358                     // that we need to cancel the original event (we can't
    359                     // do that here because the order of this loop isn't
    360                     // defined)
    361                     if (originalEvent != null && originalInstanceTimeMillis != -1) {
    362                         // The ORIGINAL_EVENT_AND_CALENDAR holds the
    363                         // calendar id concatenated with the ORIGINAL_EVENT to form
    364                         // a unique key, matching the keys for instancesMap.
    365                         initialValues.put(ORIGINAL_EVENT_AND_CALENDAR,
    366                                 CalendarInstancesHelper.getSyncIdKey(originalEvent, calendarId));
    367                         initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
    368                                 originalInstanceTimeMillis);
    369                         initialValues.put(Events.STATUS, status);
    370                     }
    371 
    372                     long dtendMillis = dtstartMillis;
    373                     if (durationStr == null) {
    374                         if (!entries.isNull(dtendColumn)) {
    375                             dtendMillis = entries.getLong(dtendColumn);
    376                         }
    377                     } else {
    378                         dtendMillis = duration.addTo(dtstartMillis);
    379                     }
    380 
    381                     // this non-recurring event might be a recurrence exception that doesn't
    382                     // actually fall within our expansion window, but instead was selected
    383                     // so we can correctly cancel expanded recurrence instances below.  do not
    384                     // add events to the instances map if they don't actually fall within our
    385                     // expansion window.
    386                     if ((dtendMillis < begin) || (dtstartMillis > end)) {
    387                         if (originalEvent != null && originalInstanceTimeMillis != -1) {
    388                             initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
    389                         } else {
    390                             if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    391                                 Log.w(CalendarProvider2.TAG, "Unexpected event outside window: "
    392                                         + syncId);
    393                             }
    394                             continue;
    395                         }
    396                     }
    397 
    398                     initialValues.put(Instances.EVENT_ID, eventId);
    399 
    400                     initialValues.put(Instances.BEGIN, dtstartMillis);
    401                     initialValues.put(Instances.END, dtendMillis);
    402 
    403                     // we temporarily store the DELETED status (will be cleaned later)
    404                     initialValues.put(Events.DELETED, deleted);
    405 
    406                     if (allDay) {
    407                         eventTime.timezone = Time.TIMEZONE_UTC;
    408                     } else {
    409                         eventTime.timezone = localTimezone;
    410                     }
    411                     CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis,
    412                             dtendMillis, eventTime, initialValues);
    413 
    414                     instancesMap.add(syncIdKey, initialValues);
    415                 }
    416             } catch (DateException e) {
    417                 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    418                     Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e);
    419                 }
    420             } catch (TimeFormatException e) {
    421                 if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
    422                     Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e);
    423                 }
    424             }
    425         }
    426 
    427         // Invariant: instancesMap contains all instances that affect the
    428         // window, indexed by original sync id concatenated with calendar id.
    429         // It consists of:
    430         // a) Individual events that fall in the window.  They have:
    431         //   EVENT_ID, BEGIN, END
    432         // b) Instances of recurrences that fall in the window.  They may
    433         //   be subject to exceptions.  They have:
    434         //   EVENT_ID, BEGIN, END
    435         // c) Exceptions that fall in the window.  They have:
    436         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can
    437         //   be a modification or cancellation), EVENT_ID, BEGIN, END
    438         // d) Recurrence exceptions that modify an instance inside the
    439         //   window but fall outside the window.  They have:
    440         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS =
    441         //   STATUS_CANCELED, EVENT_ID, BEGIN, END
    442 
    443         // First, delete the original instances corresponding to recurrence
    444         // exceptions.  We do this by iterating over the list and for each
    445         // recurrence exception, we search the list for an instance with a
    446         // matching "original instance time".  If we find such an instance,
    447         // we remove it from the list.  If we don't find such an instance
    448         // then we cancel the recurrence exception.
    449         Set<String> keys = instancesMap.keySet();
    450         for (String syncIdKey : keys) {
    451             CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey);
    452             for (ContentValues values : list) {
    453 
    454                 // If this instance is not a recurrence exception, then
    455                 // skip it.
    456                 if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) {
    457                     continue;
    458                 }
    459 
    460                 String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR);
    461                 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
    462                 CalendarInstancesHelper.InstancesList originalList = instancesMap
    463                         .get(originalEventPlusCalendar);
    464                 if (originalList == null) {
    465                     // The original recurrence is not present, so don't try canceling it.
    466                     continue;
    467                 }
    468 
    469                 // Search the original event for a matching original
    470                 // instance time.  If there is a matching one, then remove
    471                 // the original one.  We do this both for exceptions that
    472                 // change the original instance as well as for exceptions
    473                 // that delete the original instance.
    474                 for (int num = originalList.size() - 1; num >= 0; num--) {
    475                     ContentValues originalValues = originalList.get(num);
    476                     long beginTime = originalValues.getAsLong(Instances.BEGIN);
    477                     if (beginTime == originalTime) {
    478                         // We found the original instance, so remove it.
    479                         originalList.remove(num);
    480                     }
    481                 }
    482             }
    483         }
    484 
    485         // Invariant: instancesMap contains filtered instances.
    486         // It consists of:
    487         // a) Individual events that fall in the window.
    488         // b) Instances of recurrences that fall in the window and have not
    489         //   been subject to exceptions.
    490         // c) Exceptions that fall in the window.  They will have
    491         //   STATUS_CANCELED if they are cancellations.
    492         // d) Recurrence exceptions that modify an instance inside the
    493         //   window but fall outside the window.  These are STATUS_CANCELED.
    494 
    495         // Now do the inserts.  Since the db lock is held when this method is executed,
    496         // this will be done in a transaction.
    497         // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
    498         // while the calendar app is trying to query the db (expanding instances)), we will
    499         // not be "polite" and yield the lock until we're done.  This will favor local query
    500         // operations over sync/write operations.
    501         for (String syncIdKey : keys) {
    502             CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey);
    503             for (ContentValues values : list) {
    504 
    505                 // If this instance was cancelled or deleted then don't create a new
    506                 // instance.
    507                 Integer status = values.getAsInteger(Events.STATUS);
    508                 boolean deleted = values.containsKey(Events.DELETED) ?
    509                         values.getAsBoolean(Events.DELETED) : false;
    510                 if ((status != null && status == Events.STATUS_CANCELED) || deleted) {
    511                     continue;
    512                 }
    513 
    514                 // We remove this useless key (not valid in the context of Instances table)
    515                 values.remove(Events.DELETED);
    516 
    517                 // Remove these fields before inserting a new instance
    518                 values.remove(ORIGINAL_EVENT_AND_CALENDAR);
    519                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
    520                 values.remove(Events.STATUS);
    521 
    522                 mDbHelper.instancesReplace(values);
    523             }
    524         }
    525     }
    526 
    527     /**
    528      * Make instances for the given range.
    529      */
    530     protected void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
    531 
    532         if (CalendarProvider2.PROFILE) {
    533             Debug.startMethodTracing("expandInstanceRangeLocked");
    534         }
    535 
    536         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    537             Log.v(TAG, "Expanding events between " + begin + " and " + end);
    538         }
    539 
    540         Cursor entries = getEntries(begin, end);
    541         try {
    542             performInstanceExpansion(begin, end, localTimezone, entries);
    543         } finally {
    544             if (entries != null) {
    545                 entries.close();
    546             }
    547         }
    548         if (CalendarProvider2.PROFILE) {
    549             Debug.stopMethodTracing();
    550         }
    551     }
    552 
    553     /**
    554      * Get all entries affecting the given window.
    555      *
    556      * @param begin Window start (ms).
    557      * @param end Window end (ms).
    558      * @return Cursor for the entries; caller must close it.
    559      */
    560     private Cursor getEntries(long begin, long end) {
    561         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    562         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    563         qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap);
    564 
    565         String beginString = String.valueOf(begin);
    566         String endString = String.valueOf(end);
    567 
    568         // grab recurrence exceptions that fall outside our expansion window but
    569         // modify
    570         // recurrences that do fall within our window. we won't insert these
    571         // into the output
    572         // set of instances, but instead will just add them to our cancellations
    573         // list, so we
    574         // can cancel the correct recurrence expansion instances.
    575         // we don't have originalInstanceDuration or end time. for now, assume
    576         // the original
    577         // instance lasts no longer than 1 week.
    578         // also filter with syncable state (we dont want the entries from a non
    579         // syncable account)
    580         // also filter with last_synced=0 so we don't expand events that were
    581         // dup'ed for partial updates.
    582         // TODO: compute the originalInstanceEndTime or get this from the
    583         // server.
    584         qb.appendWhere(SQL_WHERE_GET_EVENTS_ENTRIES);
    585         String selectionArgs[] = new String[] {
    586                 endString,
    587                 beginString,
    588                 endString,
    589                 String.valueOf(begin - MAX_ASSUMED_DURATION),
    590                 "0", // Calendars.SYNC_EVENTS
    591                 "0", // Events.LAST_SYNCED
    592         };
    593         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    594         Cursor c = qb.query(db, EXPAND_COLUMNS, null /* selection */, selectionArgs,
    595                 null /* groupBy */, null /* having */, null /* sortOrder */);
    596         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    597             Log.v(TAG, "Instance expansion:  got " + c.getCount() + " entries");
    598         }
    599         return c;
    600     }
    601 
    602     /**
    603      * Updates the instances table when an event is added or updated.
    604      *
    605      * @param values The new values of the event.
    606      * @param rowId The database row id of the event.
    607      * @param newEvent true if the event is new.
    608      * @param db The database
    609      */
    610     public void updateInstancesLocked(ContentValues values, long rowId, boolean newEvent,
    611             SQLiteDatabase db) {
    612         /*
    613          * This may be a recurring event (has an RRULE or RDATE), an exception to a recurring
    614          * event (has ORIGINAL_ID or ORIGINAL_SYNC_ID), or a regular event.  Recurring events
    615          * and exceptions require additional handling.
    616          *
    617          * If this is not a new event, it may already have entries in Instances, so we want
    618          * to delete those before we do any additional work.
    619          */
    620 
    621         // If there are no expanded Instances, then return.
    622         MetaData.Fields fields = mMetaData.getFieldsLocked();
    623         if (fields.maxInstance == 0) {
    624             return;
    625         }
    626 
    627         Long dtstartMillis = values.getAsLong(Events.DTSTART);
    628         if (dtstartMillis == null) {
    629             if (newEvent) {
    630                 // must be present for a new event.
    631                 throw new RuntimeException("DTSTART missing.");
    632             }
    633             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    634                 Log.v(TAG, "Missing DTSTART.  No need to update instance.");
    635             }
    636             return;
    637         }
    638 
    639         if (!newEvent) {
    640             // Want to do this for regular event, recurrence, or exception.
    641             // For recurrence or exception, more deletion may happen below if we
    642             // do an instance expansion. This deletion will suffice if the
    643             // exception
    644             // is moved outside the window, for instance.
    645             db.delete(Tables.INSTANCES, Instances.EVENT_ID + "=?", new String[] {
    646                 String.valueOf(rowId)
    647             });
    648         }
    649 
    650         String rrule = values.getAsString(Events.RRULE);
    651         String rdate = values.getAsString(Events.RDATE);
    652         String originalId = values.getAsString(Events.ORIGINAL_ID);
    653         String originalSyncId = values.getAsString(Events.ORIGINAL_SYNC_ID);
    654         if (CalendarProvider2.isRecurrenceEvent(rrule, rdate, originalId, originalSyncId)) {
    655             Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
    656             Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
    657 
    658             // The recurrence or exception needs to be (re-)expanded if:
    659             // a) Exception or recurrence that falls inside window
    660             boolean insideWindow = dtstartMillis <= fields.maxInstance
    661                     && (lastDateMillis == null || lastDateMillis >= fields.minInstance);
    662             // b) Exception that affects instance inside window
    663             // These conditions match the query in getEntries
    664             // See getEntries comment for explanation of subtracting 1 week.
    665             boolean affectsWindow = originalInstanceTime != null
    666                     && originalInstanceTime <= fields.maxInstance
    667                     && originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
    668             if (CalendarProvider2.DEBUG_INSTANCES) {
    669                 Log.d(TAG + "-i", "Recurrence: inside=" + insideWindow +
    670                         ", affects=" + affectsWindow);
    671             }
    672             if (insideWindow || affectsWindow) {
    673                 updateRecurrenceInstancesLocked(values, rowId, db);
    674             }
    675             // TODO: an exception creation or update could be optimized by
    676             // updating just the affected instances, instead of regenerating
    677             // the recurrence.
    678             return;
    679         }
    680 
    681         Long dtendMillis = values.getAsLong(Events.DTEND);
    682         if (dtendMillis == null) {
    683             dtendMillis = dtstartMillis;
    684         }
    685 
    686         // if the event is in the expanded range, insert
    687         // into the instances table.
    688         // TODO: deal with durations. currently, durations are only used in
    689         // recurrences.
    690 
    691         if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
    692             ContentValues instanceValues = new ContentValues();
    693             instanceValues.put(Instances.EVENT_ID, rowId);
    694             instanceValues.put(Instances.BEGIN, dtstartMillis);
    695             instanceValues.put(Instances.END, dtendMillis);
    696 
    697             boolean allDay = false;
    698             Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
    699             if (allDayInteger != null) {
    700                 allDay = allDayInteger != 0;
    701             }
    702 
    703             // Update the timezone-dependent fields.
    704             Time local = new Time();
    705             if (allDay) {
    706                 local.timezone = Time.TIMEZONE_UTC;
    707             } else {
    708                 local.timezone = fields.timezone;
    709             }
    710 
    711             CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis, dtendMillis,
    712                     local, instanceValues);
    713             mDbHelper.instancesInsert(instanceValues);
    714         }
    715     }
    716 
    717     /**
    718      * Do incremental Instances update of a recurrence or recurrence exception.
    719      * This method does performInstanceExpansion on just the modified
    720      * recurrence, to avoid the overhead of recomputing the entire instance
    721      * table.
    722      *
    723      * @param values The new values of the event.
    724      * @param rowId The database row id of the event.
    725      * @param db The database
    726      */
    727     private void updateRecurrenceInstancesLocked(ContentValues values, long rowId,
    728             SQLiteDatabase db) {
    729         /*
    730          *  There are two categories of event that "rowId" may refer to:
    731          *  (1) Recurrence event.
    732          *  (2) Exception to recurrence event.  Has non-empty originalId (if it originated
    733          *      locally), originalSyncId (if it originated from the server), or both (if
    734          *      it's fully synchronized).
    735          *
    736          * Exceptions may arrive from the server before the recurrence event, which means:
    737          *  - We could find an originalSyncId but a lookup on originalSyncId could fail (in
    738          *    which case we can just ignore the exception for now).
    739          *  - There may be a brief period between the time we receive a recurrence and the
    740          *    time we set originalId in related exceptions where originalSyncId is the only
    741          *    way to find exceptions for a recurrence.  Thus, an empty originalId field may
    742          *    not be used to decide if an event is an exception.
    743          */
    744 
    745         MetaData.Fields fields = mMetaData.getFieldsLocked();
    746         String instancesTimezone = mCalendarCache.readTimezoneInstances();
    747 
    748         // Get the originalSyncId.  If it's not in "values", check the database.
    749         String originalSyncId = values.getAsString(Events.ORIGINAL_SYNC_ID);
    750         if (originalSyncId == null) {
    751             originalSyncId = getEventValue(db, rowId, Events.ORIGINAL_SYNC_ID);
    752         }
    753 
    754         String recurrenceSyncId;
    755         if (originalSyncId != null) {
    756             // This event is an exception; set recurrenceSyncId to the original.
    757             recurrenceSyncId = originalSyncId;
    758         } else {
    759             // This could be a recurrence or an exception.  If it has been synced with the
    760             // server we can get the _sync_id and know for certain that it's a recurrence.
    761             // If not, we'll deal with it below.
    762             recurrenceSyncId = values.getAsString(Events._SYNC_ID);
    763             if (recurrenceSyncId == null) {
    764                 // Not in "values", check the database.
    765                 recurrenceSyncId = getEventValue(db, rowId, Events._SYNC_ID);
    766             }
    767         }
    768 
    769         // Clear out old instances
    770         int delCount;
    771         if (recurrenceSyncId == null) {
    772             // We're creating or updating a recurrence or exception that hasn't been to the
    773             // server.  If this is a recurrence event, the event ID is simply the rowId.  If
    774             // it's an exception, we will find the value in the originalId field.
    775             String originalId = values.getAsString(Events.ORIGINAL_ID);
    776             if (originalId == null) {
    777                 // Not in "values", check the database.
    778                 originalId = getEventValue(db, rowId, Events.ORIGINAL_ID);
    779             }
    780             String recurrenceId;
    781             if (originalId != null) {
    782                 // This event is an exception; set recurrenceId to the original.
    783                 recurrenceId = originalId;
    784             } else {
    785                 // This event is a recurrence, so we just use the ID that was passed in.
    786                 recurrenceId = String.valueOf(rowId);
    787             }
    788 
    789             // Delete Instances entries for this Event (_id == recurrenceId) and for exceptions
    790             // to this Event (originalId == recurrenceId).
    791             String where = SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED;
    792             delCount = db.delete(Tables.INSTANCES, where, new String[] {
    793                     recurrenceId, recurrenceId
    794             });
    795         } else {
    796             // We're creating or updating a recurrence or exception that has been synced with
    797             // the server.  Delete Instances entries for this Event (_sync_id == recurrenceSyncId)
    798             // and for exceptions to this Event (originalSyncId == recurrenceSyncId).
    799             String where = SQL_WHERE_ID_FROM_INSTANCES_SYNCED;
    800             delCount = db.delete(Tables.INSTANCES, where, new String[] {
    801                     recurrenceSyncId, recurrenceSyncId
    802             });
    803         }
    804 
    805         //Log.d(TAG, "Recurrence: deleted " + delCount + " instances");
    806         //dumpInstancesTable(db);
    807 
    808         // Now do instance expansion
    809         // TODO: passing "rowId" is wrong if this is an exception - need originalId then
    810         Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
    811         try {
    812             performInstanceExpansion(fields.minInstance, fields.maxInstance,
    813                     instancesTimezone, entries);
    814         } finally {
    815             if (entries != null) {
    816                 entries.close();
    817             }
    818         }
    819     }
    820 
    821     /**
    822      * Determines the recurrence entries associated with a particular
    823      * recurrence. This set is the base recurrence and any exception. Normally
    824      * the entries are indicated by the sync id of the base recurrence (which is
    825      * the originalSyncId in the exceptions). However, a complication is that a
    826      * recurrence may not yet have a sync id. In that case, the recurrence is
    827      * specified by the rowId.
    828      *
    829      * @param recurrenceSyncId The sync id of the base recurrence, or null.
    830      * @param rowId The row id of the base recurrence.
    831      * @return the relevant entries.
    832      */
    833     private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
    834         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    835 
    836         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    837         qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap);
    838         String selectionArgs[];
    839         if (recurrenceSyncId == null) {
    840             String where = CalendarProvider2.SQL_WHERE_ID;
    841             qb.appendWhere(where);
    842             selectionArgs = new String[] {
    843                 String.valueOf(rowId)
    844             };
    845         } else {
    846             // don't expand events that were dup'ed for partial updates
    847             String where = "(" + Events._SYNC_ID + "=? OR " + Events.ORIGINAL_SYNC_ID + "=?) AND "
    848                     + Events.LAST_SYNCED + " = ?";
    849             qb.appendWhere(where);
    850             selectionArgs = new String[] {
    851                     recurrenceSyncId,
    852                     recurrenceSyncId,
    853                     "0", // Events.LAST_SYNCED
    854             };
    855         }
    856         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    857             Log.v(TAG, "Retrieving events to expand: " + qb.toString());
    858         }
    859 
    860         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    861         return qb.query(db, EXPAND_COLUMNS, null /* selection */, selectionArgs,
    862                 null /* groupBy */, null /* having */, null /* sortOrder */);
    863     }
    864 
    865     /**
    866      * Generates a unique key from the syncId and calendarId. The purpose of
    867      * this is to prevent collisions if two different calendars use the same
    868      * sync id. This can happen if a Google calendar is accessed by two
    869      * different accounts, or with Exchange, where ids are not unique between
    870      * calendars.
    871      *
    872      * @param syncId Id for the event
    873      * @param calendarId Id for the calendar
    874      * @return key
    875      */
    876     static String getSyncIdKey(String syncId, long calendarId) {
    877         return calendarId + ":" + syncId;
    878     }
    879 
    880     /**
    881      * Computes the timezone-dependent fields of an instance of an event and
    882      * updates the "values" map to contain those fields.
    883      *
    884      * @param begin the start time of the instance (in UTC milliseconds)
    885      * @param end the end time of the instance (in UTC milliseconds)
    886      * @param local a Time object with the timezone set to the local timezone
    887      * @param values a map that will contain the timezone-dependent fields
    888      */
    889     static void computeTimezoneDependentFields(long begin, long end,
    890             Time local, ContentValues values) {
    891         local.set(begin);
    892         int startDay = Time.getJulianDay(begin, local.gmtoff);
    893         int startMinute = local.hour * 60 + local.minute;
    894 
    895         local.set(end);
    896         int endDay = Time.getJulianDay(end, local.gmtoff);
    897         int endMinute = local.hour * 60 + local.minute;
    898 
    899         // Special case for midnight, which has endMinute == 0.  Change
    900         // that to +24 hours on the previous day to make everything simpler.
    901         // Exception: if start and end minute are both 0 on the same day,
    902         // then leave endMinute alone.
    903         if (endMinute == 0 && endDay > startDay) {
    904             endMinute = 24 * 60;
    905             endDay -= 1;
    906         }
    907 
    908         values.put(Instances.START_DAY, startDay);
    909         values.put(Instances.END_DAY, endDay);
    910         values.put(Instances.START_MINUTE, startMinute);
    911         values.put(Instances.END_MINUTE, endMinute);
    912     }
    913 
    914     /**
    915      * Dumps the contents of the Instances table to the log file.
    916      */
    917     private static void dumpInstancesTable(SQLiteDatabase db) {
    918         Cursor cursor = db.query(Tables.INSTANCES, null, null, null, null, null, null);
    919         DatabaseUtils.dumpCursor(cursor);
    920         if (cursor != null) {
    921             cursor.close();
    922         }
    923     }
    924 }
    925