Home | History | Annotate | Download | only in calendar
      1 /*
      2 **
      3 ** Copyright 2006, The Android Open Source Project
      4 **
      5 ** Licensed under the Apache License, Version 2.0 (the "License");
      6 ** you may not use this file except in compliance with the License.
      7 ** You may obtain a copy of the License at
      8 **
      9 **     http://www.apache.org/licenses/LICENSE-2.0
     10 **
     11 ** Unless required by applicable law or agreed to in writing, software
     12 ** distributed under the License is distributed on an "AS IS" BASIS,
     13 ** See the License for the specific language governing permissions and
     14 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15 ** limitations under the License.
     16 */
     17 
     18 package com.android.providers.calendar;
     19 
     20 import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
     21 import com.google.common.annotations.VisibleForTesting;
     22 
     23 import android.accounts.Account;
     24 import android.accounts.AccountManager;
     25 import android.accounts.OnAccountsUpdateListener;
     26 import android.app.AlarmManager;
     27 import android.app.PendingIntent;
     28 import android.content.BroadcastReceiver;
     29 import android.content.ContentResolver;
     30 import android.content.ContentUris;
     31 import android.content.ContentValues;
     32 import android.content.Context;
     33 import android.content.Intent;
     34 import android.content.IntentFilter;
     35 import android.content.UriMatcher;
     36 import android.database.Cursor;
     37 import android.database.DatabaseUtils;
     38 import android.database.SQLException;
     39 import android.database.sqlite.SQLiteDatabase;
     40 import android.database.sqlite.SQLiteQueryBuilder;
     41 import android.net.Uri;
     42 import android.os.Debug;
     43 import android.os.Process;
     44 import android.pim.EventRecurrence;
     45 import android.pim.RecurrenceSet;
     46 import android.provider.BaseColumns;
     47 import android.provider.Calendar;
     48 import android.provider.Calendar.Attendees;
     49 import android.provider.Calendar.CalendarAlerts;
     50 import android.provider.Calendar.Calendars;
     51 import android.provider.Calendar.Events;
     52 import android.provider.Calendar.Instances;
     53 import android.provider.Calendar.Reminders;
     54 import android.text.TextUtils;
     55 import android.text.format.DateUtils;
     56 import android.text.format.Time;
     57 import android.util.Log;
     58 import android.util.TimeFormatException;
     59 import android.util.TimeUtils;
     60 
     61 import java.util.ArrayList;
     62 import java.util.Arrays;
     63 import java.util.HashMap;
     64 import java.util.HashSet;
     65 import java.util.List;
     66 import java.util.Set;
     67 import java.util.TimeZone;
     68 
     69 /**
     70  * Calendar content provider. The contract between this provider and applications
     71  * is defined in {@link android.provider.Calendar}.
     72  */
     73 public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
     74 
     75     private static final String TAG = "CalendarProvider2";
     76 
     77     private static final String TIMEZONE_GMT = "GMT";
     78 
     79     private static final boolean PROFILE = false;
     80     private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
     81 
     82     private static final String INVALID_CALENDARALERTS_SELECTOR =
     83             "_id IN (SELECT ca._id FROM CalendarAlerts AS ca"
     84                     + " LEFT OUTER JOIN Instances USING (event_id, begin, end)"
     85                     + " LEFT OUTER JOIN Reminders AS r ON"
     86                     + " (ca.event_id=r.event_id AND ca.minutes=r.minutes)"
     87                     + " WHERE Instances.begin ISNULL OR ca.alarmTime<?"
     88                     + "   OR (r.minutes ISNULL AND ca.minutes<>0))";
     89 
     90     private static final String[] ID_ONLY_PROJECTION =
     91             new String[] {Events._ID};
     92 
     93     private static final String[] EVENTS_PROJECTION = new String[] {
     94             Events._SYNC_ID,
     95             Events.RRULE,
     96             Events.RDATE,
     97             Events.ORIGINAL_EVENT,
     98     };
     99     private static final int EVENTS_SYNC_ID_INDEX = 0;
    100     private static final int EVENTS_RRULE_INDEX = 1;
    101     private static final int EVENTS_RDATE_INDEX = 2;
    102     private static final int EVENTS_ORIGINAL_EVENT_INDEX = 3;
    103 
    104     private static final String[] ID_PROJECTION = new String[] {
    105             Attendees._ID,
    106             Attendees.EVENT_ID, // Assume these are the same for each table
    107     };
    108     private static final int ID_INDEX = 0;
    109     private static final int EVENT_ID_INDEX = 1;
    110 
    111     /**
    112      * Projection to query for correcting times in allDay events.
    113      */
    114     private static final String[] ALLDAY_TIME_PROJECTION = new String[] {
    115         Events._ID,
    116         Events.DTSTART,
    117         Events.DTEND,
    118         Events.DURATION
    119     };
    120     private static final int ALLDAY_ID_INDEX = 0;
    121     private static final int ALLDAY_DTSTART_INDEX = 1;
    122     private static final int ALLDAY_DTEND_INDEX = 2;
    123     private static final int ALLDAY_DURATION_INDEX = 3;
    124 
    125     private static final int DAY_IN_SECONDS = 24 * 60 * 60;
    126 
    127     /**
    128      * The cached copy of the CalendarMetaData database table.
    129      * Make this "package private" instead of "private" so that test code
    130      * can access it.
    131      */
    132     MetaData mMetaData;
    133     CalendarCache mCalendarCache;
    134 
    135     private CalendarDatabaseHelper mDbHelper;
    136 
    137     private static final Uri SYNCSTATE_CONTENT_URI = Uri.parse("content://syncstate/state");
    138     //
    139     // SCHEDULE_ALARM_URI runs scheduleNextAlarm(false)
    140     // SCHEDULE_ALARM_REMOVE_URI runs scheduleNextAlarm(true)
    141     // TODO: use a service to schedule alarms rather than private URI
    142     /* package */ static final String SCHEDULE_ALARM_PATH = "schedule_alarms";
    143     /* package */ static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove";
    144     /* package */ static final Uri SCHEDULE_ALARM_URI =
    145             Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_PATH);
    146     /* package */ static final Uri SCHEDULE_ALARM_REMOVE_URI =
    147             Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH);
    148 
    149     // 5 second delay before updating alarms
    150     private static final long ALARM_SCHEDULER_DELAY = 5000;
    151 
    152     // To determine if a recurrence exception originally overlapped the
    153     // window, we need to assume a maximum duration, since we only know
    154     // the original start time.
    155     private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
    156 
    157     // The extended property name for storing an Event original Timezone.
    158     // Due to an issue in Calendar Server restricting the length of the name we had to strip it down
    159     // TODO - Better name would be:
    160     // "com.android.providers.calendar.CalendarSyncAdapter#originalTimezone"
    161     protected static final String EXT_PROP_ORIGINAL_TIMEZONE =
    162         "CalendarSyncAdapter#originalTimezone";
    163 
    164     private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " +
    165             EventsRawTimesColumns.EVENT_ID + ", " +
    166             EventsRawTimesColumns.DTSTART_2445 + ", " +
    167             EventsRawTimesColumns.DTEND_2445 + ", " +
    168             Events.EVENT_TIMEZONE +
    169             " FROM " +
    170             "EventsRawTimes" + ", " +
    171             "Events" +
    172             " WHERE " +
    173             EventsRawTimesColumns.EVENT_ID + " = " + "Events." + Events._ID;
    174 
    175     private static final String SQL_SELECT_COUNT_FOR_SYNC_ID =
    176             "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?";
    177 
    178     private static final String SQL_WHERE_ID = BaseColumns._ID + "=?";
    179 
    180     public static final class TimeRange {
    181         public long begin;
    182         public long end;
    183         public boolean allDay;
    184     }
    185 
    186     public static final class InstancesRange {
    187         public long begin;
    188         public long end;
    189 
    190         public InstancesRange(long begin, long end) {
    191             this.begin = begin;
    192             this.end = end;
    193         }
    194     }
    195 
    196     public static final class InstancesList
    197             extends ArrayList<ContentValues> {
    198     }
    199 
    200     public static final class EventInstancesMap
    201             extends HashMap<String, InstancesList> {
    202         public void add(String syncIdKey, ContentValues values) {
    203             InstancesList instances = get(syncIdKey);
    204             if (instances == null) {
    205                 instances = new InstancesList();
    206                 put(syncIdKey, instances);
    207             }
    208             instances.add(values);
    209         }
    210     }
    211 
    212     // A thread that runs in the background and schedules the next
    213     // calendar event alarm. It delays for 5 seconds before updating
    214     // to aggregate further requests.
    215     private class AlarmScheduler extends Thread {
    216         boolean mRemoveAlarms;
    217 
    218         public AlarmScheduler(boolean removeAlarms) {
    219             mRemoveAlarms = removeAlarms;
    220         }
    221 
    222         @Override
    223         public void run() {
    224             Context context = CalendarProvider2.this.getContext();
    225             // Because the handler does not guarantee message delivery in
    226             // the case that the provider is killed, we need to make sure
    227             // that the provider stays alive long enough to deliver the
    228             // notification. This empty service is sufficient to "wedge" the
    229             // process until we finish.
    230             context.startService(new Intent(context, EmptyService.class));
    231             while (true) {
    232                 // Wait a bit before writing to collect any other requests that
    233                 // may come in
    234                 try {
    235                     sleep(ALARM_SCHEDULER_DELAY);
    236                 } catch (InterruptedException e1) {
    237                     if(Log.isLoggable(TAG, Log.DEBUG)) {
    238                         Log.d(TAG, "AlarmScheduler woke up early: " + e1.getMessage());
    239                     }
    240                 }
    241                 // Clear any new requests and update whether or not we should
    242                 // remove alarms
    243                 synchronized (mAlarmLock) {
    244                     mRemoveAlarms = mRemoveAlarms || mRemoveAlarmsOnRerun;
    245                     mRerunAlarmScheduler = false;
    246                     mRemoveAlarmsOnRerun = false;
    247                 }
    248                 // Run the update
    249                 try {
    250                     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    251                     runScheduleNextAlarm(mRemoveAlarms);
    252                 } catch (SQLException e) {
    253                     if (Log.isLoggable(TAG, Log.ERROR)) {
    254                         Log.e(TAG, "runScheduleNextAlarm() failed", e);
    255                     }
    256                 }
    257                 // Check if anyone requested another alarm change while we were busy.
    258                 // if not clear everything out and exit.
    259                 synchronized (mAlarmLock) {
    260                     if (!mRerunAlarmScheduler) {
    261                         mAlarmScheduler = null;
    262                         mRerunAlarmScheduler = false;
    263                         mRemoveAlarmsOnRerun = false;
    264                         context.stopService(new Intent(context, EmptyService.class));
    265                         return;
    266                     }
    267                 }
    268             }
    269         }
    270     }
    271 
    272     private static AlarmScheduler mAlarmScheduler;
    273 
    274     private static boolean mRerunAlarmScheduler = false;
    275     private static boolean mRemoveAlarmsOnRerun = false;
    276 
    277     /**
    278      * We search backward in time for event reminders that we may have missed
    279      * and schedule them if the event has not yet expired.  The amount in
    280      * the past to search backwards is controlled by this constant.  It
    281      * should be at least a few minutes to allow for an event that was
    282      * recently created on the web to make its way to the phone.  Two hours
    283      * might seem like overkill, but it is useful in the case where the user
    284      * just crossed into a new timezone and might have just missed an alarm.
    285      */
    286     private static final long SCHEDULE_ALARM_SLACK = 2 * DateUtils.HOUR_IN_MILLIS;
    287 
    288     /**
    289      * Alarms older than this threshold will be deleted from the CalendarAlerts
    290      * table.  This should be at least a day because if the timezone is
    291      * wrong and the user corrects it we might delete good alarms that
    292      * appear to be old because the device time was incorrectly in the future.
    293      * This threshold must also be larger than SCHEDULE_ALARM_SLACK.  We add
    294      * the SCHEDULE_ALARM_SLACK to ensure this.
    295      *
    296      * To make it easier to find and debug problems with missed reminders,
    297      * set this to something greater than a day.
    298      */
    299     private static final long CLEAR_OLD_ALARM_THRESHOLD =
    300             7 * DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK;
    301 
    302     // A lock for synchronizing access to fields that are shared
    303     // with the AlarmScheduler thread.
    304     private Object mAlarmLock = new Object();
    305 
    306     // Make sure we load at least two months worth of data.
    307     // Client apps can load more data in a background thread.
    308     private static final long MINIMUM_EXPANSION_SPAN =
    309             2L * 31 * 24 * 60 * 60 * 1000;
    310 
    311     private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
    312     private static final int CALENDARS_INDEX_ID = 0;
    313 
    314     // Allocate the string constant once here instead of on the heap
    315     private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
    316 
    317     private static final String[] sInstancesProjection =
    318             new String[] { Instances.START_DAY, Instances.END_DAY,
    319                     Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
    320 
    321     private static final int INSTANCES_INDEX_START_DAY = 0;
    322     private static final int INSTANCES_INDEX_END_DAY = 1;
    323     private static final int INSTANCES_INDEX_START_MINUTE = 2;
    324     private static final int INSTANCES_INDEX_END_MINUTE = 3;
    325     private static final int INSTANCES_INDEX_ALL_DAY = 4;
    326 
    327     private AlarmManager mAlarmManager;
    328 
    329     private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
    330 
    331     /**
    332      * Listens for timezone changes and disk-no-longer-full events
    333      */
    334     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
    335         @Override
    336         public void onReceive(Context context, Intent intent) {
    337             String action = intent.getAction();
    338             if (Log.isLoggable(TAG, Log.DEBUG)) {
    339                 Log.d(TAG, "onReceive() " + action);
    340             }
    341             if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
    342                 updateTimezoneDependentFields();
    343                 scheduleNextAlarm(false /* do not remove alarms */);
    344             } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
    345                 // Try to clean up if things were screwy due to a full disk
    346                 updateTimezoneDependentFields();
    347                 scheduleNextAlarm(false /* do not remove alarms */);
    348             } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
    349                 scheduleNextAlarm(false /* do not remove alarms */);
    350             }
    351         }
    352     };
    353 
    354     /**
    355      * Columns from the EventsRawTimes table
    356      */
    357     public interface EventsRawTimesColumns
    358     {
    359         /**
    360          * The corresponding event id
    361          * <P>Type: INTEGER (long)</P>
    362          */
    363         public static final String EVENT_ID = "event_id";
    364 
    365         /**
    366          * The RFC2445 compliant time the event starts
    367          * <P>Type: TEXT</P>
    368          */
    369         public static final String DTSTART_2445 = "dtstart2445";
    370 
    371         /**
    372          * The RFC2445 compliant time the event ends
    373          * <P>Type: TEXT</P>
    374          */
    375         public static final String DTEND_2445 = "dtend2445";
    376 
    377         /**
    378          * The RFC2445 compliant original instance time of the recurring event for which this
    379          * event is an exception.
    380          * <P>Type: TEXT</P>
    381          */
    382         public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445";
    383 
    384         /**
    385          * The RFC2445 compliant last date this event repeats on, or NULL if it never ends
    386          * <P>Type: TEXT</P>
    387          */
    388         public static final String LAST_DATE_2445 = "lastDate2445";
    389     }
    390 
    391     protected void verifyAccounts() {
    392         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
    393         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
    394     }
    395 
    396     /* Visible for testing */
    397     @Override
    398     protected CalendarDatabaseHelper getDatabaseHelper(final Context context) {
    399         return CalendarDatabaseHelper.getInstance(context);
    400     }
    401 
    402     @Override
    403     public boolean onCreate() {
    404         super.onCreate();
    405         mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper();
    406 
    407         verifyAccounts();
    408 
    409         // Register for Intent broadcasts
    410         IntentFilter filter = new IntentFilter();
    411 
    412         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    413         filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
    414         filter.addAction(Intent.ACTION_TIME_CHANGED);
    415         final Context c = getContext();
    416 
    417         // We don't ever unregister this because this thread always wants
    418         // to receive notifications, even in the background.  And if this
    419         // thread is killed then the whole process will be killed and the
    420         // memory resources will be reclaimed.
    421         c.registerReceiver(mIntentReceiver, filter);
    422 
    423         mMetaData = new MetaData(mDbHelper);
    424         mCalendarCache = new CalendarCache(mDbHelper);
    425 
    426         updateTimezoneDependentFields();
    427 
    428         return true;
    429     }
    430 
    431     /**
    432      * This creates a background thread to check the timezone and update
    433      * the timezone dependent fields in the Instances table if the timezone
    434      * has changed.
    435      */
    436     protected void updateTimezoneDependentFields() {
    437         Thread thread = new TimezoneCheckerThread();
    438         thread.start();
    439     }
    440 
    441     private class TimezoneCheckerThread extends Thread {
    442         @Override
    443         public void run() {
    444             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    445             try {
    446                 doUpdateTimezoneDependentFields();
    447                 triggerAppWidgetUpdate(-1 /*changedEventId*/ );
    448             } catch (SQLException e) {
    449                 if (Log.isLoggable(TAG, Log.ERROR)) {
    450                     Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
    451                 }
    452                 try {
    453                     // Clear at least the in-memory data (and if possible the
    454                     // database fields) to force a re-computation of Instances.
    455                     mMetaData.clearInstanceRange();
    456                 } catch (SQLException e2) {
    457                     if (Log.isLoggable(TAG, Log.ERROR)) {
    458                         Log.e(TAG, "clearInstanceRange() also failed: " + e2);
    459                     }
    460                 }
    461             }
    462         }
    463     }
    464 
    465     /**
    466      * Check if we are in the same time zone
    467      */
    468     private boolean isLocalSameAsInstancesTimezone() {
    469         String localTimezone = TimeZone.getDefault().getID();
    470         return TextUtils.equals(mCalendarCache.readTimezoneInstances(), localTimezone);
    471     }
    472 
    473     /**
    474      * This method runs in a background thread.  If the timezone db or timezone has changed
    475      * then the Instances table will be regenerated.
    476      */
    477     protected void doUpdateTimezoneDependentFields() {
    478         String timezoneType = mCalendarCache.readTimezoneType();
    479         // Nothing to do if we have the "home" timezone type (timezone is sticky)
    480         if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
    481             return;
    482         }
    483         // We are here in "auto" mode, the timezone is coming from the device
    484         if (! isSameTimezoneDatabaseVersion()) {
    485             String localTimezone = TimeZone.getDefault().getID();
    486             doProcessEventRawTimes(localTimezone, TimeUtils.getTimeZoneDatabaseVersion());
    487         }
    488         if (isLocalSameAsInstancesTimezone()) {
    489             // Even if the timezone hasn't changed, check for missed alarms.
    490             // This code executes when the CalendarProvider2 is created and
    491             // helps to catch missed alarms when the Calendar process is
    492             // killed (because of low-memory conditions) and then restarted.
    493             rescheduleMissedAlarms();
    494         }
    495     }
    496 
    497     protected void doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion) {
    498         mDb = mDbHelper.getWritableDatabase();
    499         if (mDb == null) {
    500             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    501                 Log.v(TAG, "Cannot update Events table from EventsRawTimes table");
    502             }
    503             return;
    504         }
    505         mDb.beginTransaction();
    506         try {
    507             updateEventsStartEndFromEventRawTimesLocked();
    508             updateTimezoneDatabaseVersion(timeZoneDatabaseVersion);
    509             mCalendarCache.writeTimezoneInstances(localTimezone);
    510             regenerateInstancesTable();
    511             mDb.setTransactionSuccessful();
    512         } finally {
    513             mDb.endTransaction();
    514         }
    515     }
    516 
    517     private void updateEventsStartEndFromEventRawTimesLocked() {
    518         Cursor cursor = mDb.rawQuery(SQL_SELECT_EVENTSRAWTIMES, null /* selection args */);
    519         try {
    520             while (cursor.moveToNext()) {
    521                 long eventId = cursor.getLong(0);
    522                 String dtStart2445 = cursor.getString(1);
    523                 String dtEnd2445 = cursor.getString(2);
    524                 String eventTimezone = cursor.getString(3);
    525                 if (dtStart2445 == null && dtEnd2445 == null) {
    526                     if (Log.isLoggable(TAG, Log.ERROR)) {
    527                         Log.e(TAG, "Event " + eventId + " has dtStart2445 and dtEnd2445 null "
    528                                 + "at the same time in EventsRawTimes!");
    529                     }
    530                     continue;
    531                 }
    532                 updateEventsStartEndLocked(eventId,
    533                         eventTimezone,
    534                         dtStart2445,
    535                         dtEnd2445);
    536             }
    537         } finally {
    538             cursor.close();
    539             cursor = null;
    540         }
    541     }
    542 
    543     private long get2445ToMillis(String timezone, String dt2445) {
    544         if (null == dt2445) {
    545             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    546                 Log.v( TAG, "Cannot parse null RFC2445 date");
    547             }
    548             return 0;
    549         }
    550         Time time = (timezone != null) ? new Time(timezone) : new Time();
    551         try {
    552             time.parse(dt2445);
    553         } catch (TimeFormatException e) {
    554             if (Log.isLoggable(TAG, Log.ERROR)) {
    555                 Log.e( TAG, "Cannot parse RFC2445 date " + dt2445);
    556             }
    557             return 0;
    558         }
    559         return time.toMillis(true /* ignore DST */);
    560     }
    561 
    562     private void updateEventsStartEndLocked(long eventId,
    563             String timezone, String dtStart2445, String dtEnd2445) {
    564 
    565         ContentValues values = new ContentValues();
    566         values.put("dtstart", get2445ToMillis(timezone, dtStart2445));
    567         values.put("dtend", get2445ToMillis(timezone, dtEnd2445));
    568 
    569         int result = mDb.update("Events", values, "_id=?",
    570                 new String[] {String.valueOf(eventId)});
    571         if (0 == result) {
    572             if (Log.isLoggable(TAG, Log.VERBOSE)) {
    573                 Log.v(TAG, "Could not update Events table with values " + values);
    574             }
    575         }
    576     }
    577 
    578     private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) {
    579         try {
    580             mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion);
    581         } catch (CalendarCache.CacheException e) {
    582             if (Log.isLoggable(TAG, Log.ERROR)) {
    583                 Log.e(TAG, "Could not write timezone database version in the cache");
    584             }
    585         }
    586     }
    587 
    588     /**
    589      * Check if the time zone database version is the same as the cached one
    590      */
    591     protected boolean isSameTimezoneDatabaseVersion() {
    592         String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
    593         if (timezoneDatabaseVersion == null) {
    594             return false;
    595         }
    596         return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion());
    597     }
    598 
    599     @VisibleForTesting
    600     protected String getTimezoneDatabaseVersion() {
    601         String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
    602         if (timezoneDatabaseVersion == null) {
    603             return "";
    604         }
    605         if (Log.isLoggable(TAG, Log.INFO)) {
    606             Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion);
    607         }
    608         return timezoneDatabaseVersion;
    609     }
    610 
    611     private boolean isHomeTimezone() {
    612         String type = mCalendarCache.readTimezoneType();
    613         return type.equals(CalendarCache.TIMEZONE_TYPE_HOME);
    614     }
    615 
    616     private void regenerateInstancesTable() {
    617         // The database timezone is different from the current timezone.
    618         // Regenerate the Instances table for this month.  Include events
    619         // starting at the beginning of this month.
    620         long now = System.currentTimeMillis();
    621         String instancesTimezone = mCalendarCache.readTimezoneInstances();
    622         Time time = new Time(instancesTimezone);
    623         time.set(now);
    624         time.monthDay = 1;
    625         time.hour = 0;
    626         time.minute = 0;
    627         time.second = 0;
    628 
    629         long begin = time.normalize(true);
    630         long end = begin + MINIMUM_EXPANSION_SPAN;
    631 
    632         Cursor cursor = null;
    633         try {
    634             cursor = handleInstanceQuery(new SQLiteQueryBuilder(),
    635                     begin, end,
    636                     new String[] { Instances._ID },
    637                     null /* selection */, null /* sort */,
    638                     false /* searchByDayInsteadOfMillis */,
    639                     true /* force Instances deletion and expansion */,
    640                     instancesTimezone,
    641                     isHomeTimezone());
    642         } finally {
    643             if (cursor != null) {
    644                 cursor.close();
    645             }
    646         }
    647 
    648         rescheduleMissedAlarms();
    649     }
    650 
    651     private void rescheduleMissedAlarms() {
    652         AlarmManager manager = getAlarmManager();
    653         if (manager != null) {
    654             Context context = getContext();
    655             ContentResolver cr = context.getContentResolver();
    656             CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
    657         }
    658     }
    659 
    660     /**
    661      * Appends comma separated ids.
    662      * @param ids Should not be empty
    663      */
    664     private void appendIds(StringBuilder sb, HashSet<Long> ids) {
    665         for (long id : ids) {
    666             sb.append(id).append(',');
    667         }
    668 
    669         sb.setLength(sb.length() - 1); // Yank the last comma
    670     }
    671 
    672     @Override
    673     protected void notifyChange() {
    674         // Note that semantics are changed: notification is for CONTENT_URI, not the specific
    675         // Uri that was modified.
    676         getContext().getContentResolver().notifyChange(Calendar.CONTENT_URI, null,
    677                 true /* syncToNetwork */);
    678     }
    679 
    680     @Override
    681     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    682             String sortOrder) {
    683         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    684             Log.v(TAG, "query uri - " + uri);
    685         }
    686 
    687         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
    688 
    689         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    690         String groupBy = null;
    691         String limit = null; // Not currently implemented
    692         String instancesTimezone;
    693 
    694         final int match = sUriMatcher.match(uri);
    695         switch (match) {
    696             case SYNCSTATE:
    697                 return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
    698                         sortOrder);
    699 
    700             case EVENTS:
    701                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    702                 qb.setProjectionMap(sEventsProjectionMap);
    703                 appendAccountFromParameter(qb, uri);
    704                 break;
    705             case EVENTS_ID:
    706                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    707                 qb.setProjectionMap(sEventsProjectionMap);
    708                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
    709                 qb.appendWhere("_id=?");
    710                 break;
    711 
    712             case EVENT_ENTITIES:
    713                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    714                 qb.setProjectionMap(sEventEntitiesProjectionMap);
    715                 appendAccountFromParameter(qb, uri);
    716                 break;
    717             case EVENT_ENTITIES_ID:
    718                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
    719                 qb.setProjectionMap(sEventEntitiesProjectionMap);
    720                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
    721                 qb.appendWhere("_id=?");
    722                 break;
    723 
    724             case CALENDARS:
    725                 qb.setTables("Calendars");
    726                 appendAccountFromParameter(qb, uri);
    727                 break;
    728             case CALENDARS_ID:
    729                 qb.setTables("Calendars");
    730                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
    731                 qb.appendWhere("_id=?");
    732                 break;
    733             case INSTANCES:
    734             case INSTANCES_BY_DAY:
    735                 long begin;
    736                 long end;
    737                 try {
    738                     begin = Long.valueOf(uri.getPathSegments().get(2));
    739                 } catch (NumberFormatException nfe) {
    740                     throw new IllegalArgumentException("Cannot parse begin "
    741                             + uri.getPathSegments().get(2));
    742                 }
    743                 try {
    744                     end = Long.valueOf(uri.getPathSegments().get(3));
    745                 } catch (NumberFormatException nfe) {
    746                     throw new IllegalArgumentException("Cannot parse end "
    747                             + uri.getPathSegments().get(3));
    748                 }
    749                 instancesTimezone = mCalendarCache.readTimezoneInstances();
    750                 return handleInstanceQuery(qb, begin, end, projection,
    751                         selection, sortOrder, match == INSTANCES_BY_DAY,
    752                         false /* do not force Instances deletion and expansion */,
    753                         instancesTimezone, isHomeTimezone());
    754             case EVENT_DAYS:
    755                 int startDay;
    756                 int endDay;
    757                 try {
    758                     startDay = Integer.valueOf(uri.getPathSegments().get(2));
    759                 } catch (NumberFormatException nfe) {
    760                     throw new IllegalArgumentException("Cannot parse start day "
    761                             + uri.getPathSegments().get(2));
    762                 }
    763                 try {
    764                     endDay = Integer.valueOf(uri.getPathSegments().get(3));
    765                 } catch (NumberFormatException nfe) {
    766                     throw new IllegalArgumentException("Cannot parse end day "
    767                             + uri.getPathSegments().get(3));
    768                 }
    769                 instancesTimezone = mCalendarCache.readTimezoneInstances();
    770                 return handleEventDayQuery(qb, startDay, endDay, projection, selection,
    771                         instancesTimezone, isHomeTimezone());
    772             case ATTENDEES:
    773                 qb.setTables("Attendees, Events");
    774                 qb.setProjectionMap(sAttendeesProjectionMap);
    775                 qb.appendWhere("Events._id=Attendees.event_id");
    776                 break;
    777             case ATTENDEES_ID:
    778                 qb.setTables("Attendees, Events");
    779                 qb.setProjectionMap(sAttendeesProjectionMap);
    780                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
    781                 qb.appendWhere("Attendees._id=?  AND Events._id=Attendees.event_id");
    782                 break;
    783             case REMINDERS:
    784                 qb.setTables("Reminders");
    785                 break;
    786             case REMINDERS_ID:
    787                 qb.setTables("Reminders, Events");
    788                 qb.setProjectionMap(sRemindersProjectionMap);
    789                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
    790                 qb.appendWhere("Reminders._id=? AND Events._id=Reminders.event_id");
    791                 break;
    792             case CALENDAR_ALERTS:
    793                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
    794                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
    795                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
    796                         "._id=CalendarAlerts.event_id");
    797                 break;
    798             case CALENDAR_ALERTS_BY_INSTANCE:
    799                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
    800                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
    801                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
    802                         "._id=CalendarAlerts.event_id");
    803                 groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
    804                 break;
    805             case CALENDAR_ALERTS_ID:
    806                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
    807                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
    808                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
    809                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
    810                         "._id=CalendarAlerts.event_id AND CalendarAlerts._id=?");
    811                 break;
    812             case EXTENDED_PROPERTIES:
    813                 qb.setTables("ExtendedProperties");
    814                 break;
    815             case EXTENDED_PROPERTIES_ID:
    816                 qb.setTables("ExtendedProperties");
    817                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
    818                 qb.appendWhere("ExtendedProperties._id=?");
    819                 break;
    820             case PROVIDER_PROPERTIES:
    821                 qb.setTables("CalendarCache");
    822                 qb.setProjectionMap(sCalendarCacheProjectionMap);
    823                 break;
    824             default:
    825                 throw new IllegalArgumentException("Unknown URL " + uri);
    826         }
    827 
    828         // run the query
    829         return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
    830     }
    831 
    832     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
    833             String selection, String[] selectionArgs, String sortOrder, String groupBy,
    834             String limit) {
    835 
    836         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    837             Log.v(TAG, "query sql - projection: " + Arrays.toString(projection) +
    838                     " selection: " + selection +
    839                     " selectionArgs: " + Arrays.toString(selectionArgs) +
    840                     " sortOrder: " + sortOrder +
    841                     " groupBy: " + groupBy +
    842                     " limit: " + limit);
    843         }
    844         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
    845                 sortOrder, limit);
    846         if (c != null) {
    847             // TODO: is this the right notification Uri?
    848             c.setNotificationUri(getContext().getContentResolver(), Calendar.Events.CONTENT_URI);
    849         }
    850         return c;
    851     }
    852 
    853     /*
    854      * Fills the Instances table, if necessary, for the given range and then
    855      * queries the Instances table.
    856      *
    857      * @param qb The query
    858      * @param rangeBegin start of range (Julian days or ms)
    859      * @param rangeEnd end of range (Julian days or ms)
    860      * @param projection The projection
    861      * @param selection The selection
    862      * @param sort How to sort
    863      * @param searchByDay if true, range is in Julian days, if false, range is in ms
    864      * @param forceExpansion force the Instance deletion and expansion if set to true
    865      * @param instancesTimezone timezone we need to use for computing the instances
    866      * @param isHomeTimezone if true, we are in the "home" timezone
    867      * @return
    868      */
    869     private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
    870             long rangeEnd, String[] projection, String selection, String sort,
    871             boolean searchByDay, boolean forceExpansion, String instancesTimezone,
    872             boolean isHomeTimezone) {
    873 
    874         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
    875                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
    876         qb.setProjectionMap(sInstancesProjectionMap);
    877         if (searchByDay) {
    878             // Convert the first and last Julian day range to a range that uses
    879             // UTC milliseconds.
    880             Time time = new Time(instancesTimezone);
    881             long beginMs = time.setJulianDay((int) rangeBegin);
    882             // We add one to lastDay because the time is set to 12am on the given
    883             // Julian day and we want to include all the events on the last day.
    884             long endMs = time.setJulianDay((int) rangeEnd + 1);
    885             // will lock the database.
    886             acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */,
    887                     forceExpansion, instancesTimezone, isHomeTimezone
    888             );
    889             qb.appendWhere("startDay<=? AND endDay>=?");
    890         } else {
    891             // will lock the database.
    892             acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */,
    893                     forceExpansion, instancesTimezone, isHomeTimezone
    894             );
    895             qb.appendWhere("begin<=? AND end>=?");
    896         }
    897         String selectionArgs[] = new String[] {String.valueOf(rangeEnd),
    898                 String.valueOf(rangeBegin)};
    899         return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */,
    900                 null /* having */, sort);
    901     }
    902 
    903     private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end,
    904             String[] projection, String selection, String instancesTimezone,
    905             boolean isHomeTimezone) {
    906         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
    907                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
    908         qb.setProjectionMap(sInstancesProjectionMap);
    909         // Convert the first and last Julian day range to a range that uses
    910         // UTC milliseconds.
    911         Time time = new Time(instancesTimezone);
    912         long beginMs = time.setJulianDay(begin);
    913         // We add one to lastDay because the time is set to 12am on the given
    914         // Julian day and we want to include all the events on the last day.
    915         long endMs = time.setJulianDay(end + 1);
    916 
    917         acquireInstanceRange(beginMs, endMs, true,
    918                 false /* do not force Instances expansion */, instancesTimezone, isHomeTimezone);
    919         qb.appendWhere("startDay<=? AND endDay>=?");
    920         String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)};
    921 
    922         return qb.query(mDb, projection, selection, selectionArgs,
    923                 Instances.START_DAY /* groupBy */, null /* having */, null);
    924     }
    925 
    926     /**
    927      * Ensure that the date range given has all elements in the instance
    928      * table.  Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
    929      *
    930      * @param begin start of range (ms)
    931      * @param end end of range (ms)
    932      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
    933      * @param forceExpansion force the Instance deletion and expansion if set to true
    934      * @param instancesTimezone timezone we need to use for computing the instances
    935      * @param isHomeTimezone if true, we are in the "home" timezone
    936      */
    937     private void acquireInstanceRange(final long begin, final long end,
    938             final boolean useMinimumExpansionWindow, final boolean forceExpansion,
    939             final String instancesTimezone, final boolean isHomeTimezone) {
    940         mDb.beginTransaction();
    941         try {
    942             acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow,
    943                     forceExpansion, instancesTimezone, isHomeTimezone);
    944             mDb.setTransactionSuccessful();
    945         } finally {
    946             mDb.endTransaction();
    947         }
    948     }
    949 
    950     /**
    951      * Ensure that the date range given has all elements in the instance
    952      * table.  The database lock must be held when calling this method.
    953      *
    954      * @param begin start of range (ms)
    955      * @param end end of range (ms)
    956      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
    957      * @param forceExpansion force the Instance deletion and expansion if set to true
    958      * @param instancesTimezone timezone we need to use for computing the instances
    959      * @param isHomeTimezone if true, we are in the "home" timezone
    960      */
    961     private void acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow,
    962             boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone) {
    963         long expandBegin = begin;
    964         long expandEnd = end;
    965 
    966         if (instancesTimezone == null) {
    967             if (Log.isLoggable(TAG, Log.ERROR)) {
    968                 Log.e(TAG, "Cannot run acquireInstanceRangeLocked() "
    969                         + "because instancesTimezone is null");
    970             }
    971             return;
    972         }
    973 
    974         if (useMinimumExpansionWindow) {
    975             // if we end up having to expand events into the instances table, expand
    976             // events for a minimal amount of time, so we do not have to perform
    977             // expansions frequently.
    978             long span = end - begin;
    979             if (span < MINIMUM_EXPANSION_SPAN) {
    980                 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
    981                 expandBegin -= additionalRange;
    982                 expandEnd += additionalRange;
    983             }
    984         }
    985 
    986         // Check if the timezone has changed.
    987         // We do this check here because the database is locked and we can
    988         // safely delete all the entries in the Instances table.
    989         MetaData.Fields fields = mMetaData.getFieldsLocked();
    990         long maxInstance = fields.maxInstance;
    991         long minInstance = fields.minInstance;
    992         boolean timezoneChanged;
    993         if (isHomeTimezone) {
    994             String previousTimezone = mCalendarCache.readTimezoneInstancesPrevious();
    995             timezoneChanged = !instancesTimezone.equals(previousTimezone);
    996         } else {
    997             String localTimezone = TimeZone.getDefault().getID();
    998             timezoneChanged = !instancesTimezone.equals(localTimezone);
    999             // if we're in auto make sure we are using the device time zone
   1000             if (timezoneChanged) {
   1001                 instancesTimezone = localTimezone;
   1002             }
   1003         }
   1004         // if "home", then timezoneChanged only if current != previous
   1005         // if "auto", then timezoneChanged, if !instancesTimezone.equals(localTimezone);
   1006         if (maxInstance == 0 || timezoneChanged || forceExpansion) {
   1007             // Empty the Instances table and expand from scratch.
   1008             mDb.execSQL("DELETE FROM Instances;");
   1009             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1010                 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances,"
   1011                         + " timezone changed: " + timezoneChanged);
   1012             }
   1013             expandInstanceRangeLocked(expandBegin, expandEnd, instancesTimezone);
   1014 
   1015             mMetaData.writeLocked(instancesTimezone, expandBegin, expandEnd);
   1016 
   1017             String timezoneType = mCalendarCache.readTimezoneType();
   1018             // This may cause some double writes but guarantees the time zone in
   1019             // the db and the time zone the instances are in is the same, which
   1020             // future changes may affect.
   1021             mCalendarCache.writeTimezoneInstances(instancesTimezone);
   1022 
   1023             // If we're in auto check if we need to fix the previous tz value
   1024             if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
   1025                 String prevTZ = mCalendarCache.readTimezoneInstancesPrevious();
   1026                 if (TextUtils.equals(TIMEZONE_GMT, prevTZ)) {
   1027                     mCalendarCache.writeTimezoneInstancesPrevious(instancesTimezone);
   1028                 }
   1029             }
   1030             return;
   1031         }
   1032 
   1033         // If the desired range [begin, end] has already been
   1034         // expanded, then simply return.  The range is inclusive, that is,
   1035         // events that touch either endpoint are included in the expansion.
   1036         // This means that a zero-duration event that starts and ends at
   1037         // the endpoint will be included.
   1038         // We use [begin, end] here and not [expandBegin, expandEnd] for
   1039         // checking the range because a common case is for the client to
   1040         // request successive days or weeks, for example.  If we checked
   1041         // that the expanded range [expandBegin, expandEnd] then we would
   1042         // always be expanding because there would always be one more day
   1043         // or week that hasn't been expanded.
   1044         if ((begin >= minInstance) && (end <= maxInstance)) {
   1045             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1046                 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
   1047                         + ") falls within previously expanded range.");
   1048             }
   1049             return;
   1050         }
   1051 
   1052         // If the requested begin point has not been expanded, then include
   1053         // more events than requested in the expansion (use "expandBegin").
   1054         if (begin < minInstance) {
   1055             expandInstanceRangeLocked(expandBegin, minInstance, instancesTimezone);
   1056             minInstance = expandBegin;
   1057         }
   1058 
   1059         // If the requested end point has not been expanded, then include
   1060         // more events than requested in the expansion (use "expandEnd").
   1061         if (end > maxInstance) {
   1062             expandInstanceRangeLocked(maxInstance, expandEnd, instancesTimezone);
   1063             maxInstance = expandEnd;
   1064         }
   1065 
   1066         // Update the bounds on the Instances table (timezone is the same here)
   1067         mMetaData.writeLocked(instancesTimezone, minInstance, maxInstance);
   1068     }
   1069 
   1070     private static final String[] EXPAND_COLUMNS = new String[] {
   1071             Events._ID,
   1072             Events._SYNC_ID,
   1073             Events.STATUS,
   1074             Events.DTSTART,
   1075             Events.DTEND,
   1076             Events.EVENT_TIMEZONE,
   1077             Events.RRULE,
   1078             Events.RDATE,
   1079             Events.EXRULE,
   1080             Events.EXDATE,
   1081             Events.DURATION,
   1082             Events.ALL_DAY,
   1083             Events.ORIGINAL_EVENT,
   1084             Events.ORIGINAL_INSTANCE_TIME,
   1085             Events.CALENDAR_ID,
   1086             Events.DELETED
   1087     };
   1088 
   1089     /**
   1090      * Make instances for the given range.
   1091      */
   1092     private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
   1093 
   1094         if (PROFILE) {
   1095             Debug.startMethodTracing("expandInstanceRangeLocked");
   1096         }
   1097 
   1098         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1099             Log.v(TAG, "Expanding events between " + begin + " and " + end);
   1100         }
   1101 
   1102         Cursor entries = getEntries(begin, end);
   1103         try {
   1104             performInstanceExpansion(begin, end, localTimezone, entries);
   1105         } finally {
   1106             if (entries != null) {
   1107                 entries.close();
   1108             }
   1109         }
   1110         if (PROFILE) {
   1111             Debug.stopMethodTracing();
   1112         }
   1113     }
   1114 
   1115     /**
   1116      * Get all entries affecting the given window.
   1117      * @param begin Window start (ms).
   1118      * @param end Window end (ms).
   1119      * @return Cursor for the entries; caller must close it.
   1120      */
   1121     private Cursor getEntries(long begin, long end) {
   1122         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   1123         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
   1124         qb.setProjectionMap(sEventsProjectionMap);
   1125 
   1126         String beginString = String.valueOf(begin);
   1127         String endString = String.valueOf(end);
   1128 
   1129         // grab recurrence exceptions that fall outside our expansion window but modify
   1130         // recurrences that do fall within our window.  we won't insert these into the output
   1131         // set of instances, but instead will just add them to our cancellations list, so we
   1132         // can cancel the correct recurrence expansion instances.
   1133         // we don't have originalInstanceDuration or end time.  for now, assume the original
   1134         // instance lasts no longer than 1 week.
   1135         // also filter with syncable state (we dont want the entries from a non syncable account)
   1136         // TODO: compute the originalInstanceEndTime or get this from the server.
   1137         qb.appendWhere("((dtstart <= ? AND (lastDate IS NULL OR lastDate >= ?)) OR " +
   1138                 "(originalInstanceTime IS NOT NULL AND originalInstanceTime <= ? AND " +
   1139                 "originalInstanceTime >= ?)) AND (sync_events != 0)");
   1140         String selectionArgs[] = new String[] {endString, beginString, endString,
   1141                 String.valueOf(begin - MAX_ASSUMED_DURATION)};
   1142         Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */,
   1143                 selectionArgs, null /* groupBy */,
   1144                 null /* having */, null /* sortOrder */);
   1145         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1146             Log.v(TAG, "Instance expansion:  got " + c.getCount() + " entries");
   1147         }
   1148         return c;
   1149     }
   1150 
   1151     /**
   1152      * Generates a unique key from the syncId and calendarId.
   1153      * The purpose of this is to prevent collisions if two different calendars use the
   1154      * same sync id.  This can happen if a Google calendar is accessed by two different accounts,
   1155      * or with Exchange, where ids are not unique between calendars.
   1156      * @param syncId Id for the event
   1157      * @param calendarId Id for the calendar
   1158      * @return key
   1159      */
   1160     private String getSyncIdKey(String syncId, long calendarId) {
   1161         return calendarId + ":" + syncId;
   1162     }
   1163 
   1164     /**
   1165      * Perform instance expansion on the given entries.
   1166      * @param begin Window start (ms).
   1167      * @param end Window end (ms).
   1168      * @param localTimezone
   1169      * @param entries The entries to process.
   1170      */
   1171     private void performInstanceExpansion(long begin, long end, String localTimezone,
   1172                                           Cursor entries) {
   1173         RecurrenceProcessor rp = new RecurrenceProcessor();
   1174 
   1175         // Key into the instance values to hold the original event concatenated with calendar id.
   1176         final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR";
   1177 
   1178         int statusColumn = entries.getColumnIndex(Events.STATUS);
   1179         int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
   1180         int dtendColumn = entries.getColumnIndex(Events.DTEND);
   1181         int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
   1182         int durationColumn = entries.getColumnIndex(Events.DURATION);
   1183         int rruleColumn = entries.getColumnIndex(Events.RRULE);
   1184         int rdateColumn = entries.getColumnIndex(Events.RDATE);
   1185         int exruleColumn = entries.getColumnIndex(Events.EXRULE);
   1186         int exdateColumn = entries.getColumnIndex(Events.EXDATE);
   1187         int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
   1188         int idColumn = entries.getColumnIndex(Events._ID);
   1189         int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
   1190         int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
   1191         int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
   1192         int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID);
   1193         int deletedColumn = entries.getColumnIndex(Events.DELETED);
   1194 
   1195         ContentValues initialValues;
   1196         EventInstancesMap instancesMap = new EventInstancesMap();
   1197 
   1198         Duration duration = new Duration();
   1199         Time eventTime = new Time();
   1200 
   1201         // Invariant: entries contains all events that affect the current
   1202         // window.  It consists of:
   1203         // a) Individual events that fall in the window.  These will be
   1204         //    displayed.
   1205         // b) Recurrences that included the window.  These will be displayed
   1206         //    if not canceled.
   1207         // c) Recurrence exceptions that fall in the window.  These will be
   1208         //    displayed if not cancellations.
   1209         // d) Recurrence exceptions that modify an instance inside the
   1210         //    window (subject to 1 week assumption above), but are outside
   1211         //    the window.  These will not be displayed.  Cases c and d are
   1212         //    distingushed by the start / end time.
   1213 
   1214         while (entries.moveToNext()) {
   1215             try {
   1216                 initialValues = null;
   1217 
   1218                 boolean allDay = entries.getInt(allDayColumn) != 0;
   1219 
   1220                 String eventTimezone = entries.getString(eventTimezoneColumn);
   1221                 if (allDay || TextUtils.isEmpty(eventTimezone)) {
   1222                     // in the events table, allDay events start at midnight.
   1223                     // this forces them to stay at midnight for all day events
   1224                     // TODO: check that this actually does the right thing.
   1225                     eventTimezone = Time.TIMEZONE_UTC;
   1226                 }
   1227 
   1228                 long dtstartMillis = entries.getLong(dtstartColumn);
   1229                 Long eventId = Long.valueOf(entries.getLong(idColumn));
   1230 
   1231                 String durationStr = entries.getString(durationColumn);
   1232                 if (durationStr != null) {
   1233                     try {
   1234                         duration.parse(durationStr);
   1235                     }
   1236                     catch (DateException e) {
   1237                         if (Log.isLoggable(TAG, Log.WARN)) {
   1238                             Log.w(TAG, "error parsing duration for event "
   1239                                     + eventId + "'" + durationStr + "'", e);
   1240                         }
   1241                         duration.sign = 1;
   1242                         duration.weeks = 0;
   1243                         duration.days = 0;
   1244                         duration.hours = 0;
   1245                         duration.minutes = 0;
   1246                         duration.seconds = 0;
   1247                         durationStr = "+P0S";
   1248                     }
   1249                 }
   1250 
   1251                 String syncId = entries.getString(syncIdColumn);
   1252                 String originalEvent = entries.getString(originalEventColumn);
   1253 
   1254                 long originalInstanceTimeMillis = -1;
   1255                 if (!entries.isNull(originalInstanceTimeColumn)) {
   1256                     originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
   1257                 }
   1258                 int status = entries.getInt(statusColumn);
   1259                 boolean deleted = (entries.getInt(deletedColumn) != 0);
   1260 
   1261                 String rruleStr = entries.getString(rruleColumn);
   1262                 String rdateStr = entries.getString(rdateColumn);
   1263                 String exruleStr = entries.getString(exruleColumn);
   1264                 String exdateStr = entries.getString(exdateColumn);
   1265                 long calendarId = entries.getLong(calendarIdColumn);
   1266                 String syncIdKey = getSyncIdKey(syncId, calendarId); // key into instancesMap
   1267 
   1268                 RecurrenceSet recur = null;
   1269                 try {
   1270                     recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
   1271                 } catch (EventRecurrence.InvalidFormatException e) {
   1272                     if (Log.isLoggable(TAG, Log.WARN)) {
   1273                         Log.w(TAG, "Could not parse RRULE recurrence string: " + rruleStr, e);
   1274                     }
   1275                     continue;
   1276                 }
   1277 
   1278                 if (null != recur && recur.hasRecurrence()) {
   1279                     // the event is repeating
   1280 
   1281                     if (status == Events.STATUS_CANCELED) {
   1282                         // should not happen!
   1283                         if (Log.isLoggable(TAG, Log.ERROR)) {
   1284                             Log.e(TAG, "Found canceled recurring event in "
   1285                                     + "Events table.  Ignoring.");
   1286                         }
   1287                         continue;
   1288                     }
   1289 
   1290                     if (deleted) {
   1291                         if (Log.isLoggable(TAG, Log.DEBUG)) {
   1292                             Log.d(TAG, "Found deleted recurring event in "
   1293                                     + "Events table.  Ignoring.");
   1294                         }
   1295                         continue;
   1296                     }
   1297 
   1298                     // need to parse the event into a local calendar.
   1299                     eventTime.timezone = eventTimezone;
   1300                     eventTime.set(dtstartMillis);
   1301                     eventTime.allDay = allDay;
   1302 
   1303                     if (durationStr == null) {
   1304                         // should not happen.
   1305                         if (Log.isLoggable(TAG, Log.ERROR)) {
   1306                             Log.e(TAG, "Repeating event has no duration -- "
   1307                                     + "should not happen.");
   1308                         }
   1309                         if (allDay) {
   1310                             // set to one day.
   1311                             duration.sign = 1;
   1312                             duration.weeks = 0;
   1313                             duration.days = 1;
   1314                             duration.hours = 0;
   1315                             duration.minutes = 0;
   1316                             duration.seconds = 0;
   1317                             durationStr = "+P1D";
   1318                         } else {
   1319                             // compute the duration from dtend, if we can.
   1320                             // otherwise, use 0s.
   1321                             duration.sign = 1;
   1322                             duration.weeks = 0;
   1323                             duration.days = 0;
   1324                             duration.hours = 0;
   1325                             duration.minutes = 0;
   1326                             if (!entries.isNull(dtendColumn)) {
   1327                                 long dtendMillis = entries.getLong(dtendColumn);
   1328                                 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
   1329                                 durationStr = "+P" + duration.seconds + "S";
   1330                             } else {
   1331                                 duration.seconds = 0;
   1332                                 durationStr = "+P0S";
   1333                             }
   1334                         }
   1335                     }
   1336 
   1337                     long[] dates;
   1338                     dates = rp.expand(eventTime, recur, begin, end);
   1339 
   1340                     // Initialize the "eventTime" timezone outside the loop.
   1341                     // This is used in computeTimezoneDependentFields().
   1342                     if (allDay) {
   1343                         eventTime.timezone = Time.TIMEZONE_UTC;
   1344                     } else {
   1345                         eventTime.timezone = localTimezone;
   1346                     }
   1347 
   1348                     long durationMillis = duration.getMillis();
   1349                     for (long date : dates) {
   1350                         initialValues = new ContentValues();
   1351                         initialValues.put(Instances.EVENT_ID, eventId);
   1352 
   1353                         initialValues.put(Instances.BEGIN, date);
   1354                         long dtendMillis = date + durationMillis;
   1355                         initialValues.put(Instances.END, dtendMillis);
   1356 
   1357                         computeTimezoneDependentFields(date, dtendMillis,
   1358                                 eventTime, initialValues);
   1359                         instancesMap.add(syncIdKey, initialValues);
   1360                     }
   1361                 } else {
   1362                     // the event is not repeating
   1363                     initialValues = new ContentValues();
   1364 
   1365                     // if this event has an "original" field, then record
   1366                     // that we need to cancel the original event (we can't
   1367                     // do that here because the order of this loop isn't
   1368                     // defined)
   1369                     if (originalEvent != null && originalInstanceTimeMillis != -1) {
   1370                         // The ORIGINAL_EVENT_AND_CALENDAR holds the
   1371                         // calendar id concatenated with the ORIGINAL_EVENT to form
   1372                         // a unique key, matching the keys for instancesMap.
   1373                         initialValues.put(ORIGINAL_EVENT_AND_CALENDAR,
   1374                                 getSyncIdKey(originalEvent, calendarId));
   1375                         initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
   1376                                 originalInstanceTimeMillis);
   1377                         initialValues.put(Events.STATUS, status);
   1378                     }
   1379 
   1380                     long dtendMillis = dtstartMillis;
   1381                     if (durationStr == null) {
   1382                         if (!entries.isNull(dtendColumn)) {
   1383                             dtendMillis = entries.getLong(dtendColumn);
   1384                         }
   1385                     } else {
   1386                         dtendMillis = duration.addTo(dtstartMillis);
   1387                     }
   1388 
   1389                     // this non-recurring event might be a recurrence exception that doesn't
   1390                     // actually fall within our expansion window, but instead was selected
   1391                     // so we can correctly cancel expanded recurrence instances below.  do not
   1392                     // add events to the instances map if they don't actually fall within our
   1393                     // expansion window.
   1394                     if ((dtendMillis < begin) || (dtstartMillis > end)) {
   1395                         if (originalEvent != null && originalInstanceTimeMillis != -1) {
   1396                             initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
   1397                         } else {
   1398                             if (Log.isLoggable(TAG, Log.WARN)) {
   1399                                 Log.w(TAG, "Unexpected event outside window: " + syncId);
   1400                             }
   1401                             continue;
   1402                         }
   1403                     }
   1404 
   1405                     initialValues.put(Instances.EVENT_ID, eventId);
   1406 
   1407                     initialValues.put(Instances.BEGIN, dtstartMillis);
   1408                     initialValues.put(Instances.END, dtendMillis);
   1409 
   1410                     // we temporarily store the DELETED status (will be cleaned later)
   1411                     initialValues.put(Events.DELETED, deleted);
   1412 
   1413                     if (allDay) {
   1414                         eventTime.timezone = Time.TIMEZONE_UTC;
   1415                     } else {
   1416                         eventTime.timezone = localTimezone;
   1417                     }
   1418                     computeTimezoneDependentFields(dtstartMillis, dtendMillis,
   1419                             eventTime, initialValues);
   1420 
   1421                     instancesMap.add(syncIdKey, initialValues);
   1422                 }
   1423             } catch (DateException e) {
   1424                 if (Log.isLoggable(TAG, Log.WARN)) {
   1425                     Log.w(TAG, "RecurrenceProcessor error ", e);
   1426                 }
   1427             } catch (TimeFormatException e) {
   1428                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1429                     Log.w(TAG, "RecurrenceProcessor error ", e);
   1430                 }
   1431             }
   1432         }
   1433 
   1434         // Invariant: instancesMap contains all instances that affect the
   1435         // window, indexed by original sync id concatenated with calendar id.
   1436         // It consists of:
   1437         // a) Individual events that fall in the window.  They have:
   1438         //   EVENT_ID, BEGIN, END
   1439         // b) Instances of recurrences that fall in the window.  They may
   1440         //   be subject to exceptions.  They have:
   1441         //   EVENT_ID, BEGIN, END
   1442         // c) Exceptions that fall in the window.  They have:
   1443         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can
   1444         //   be a modification or cancellation), EVENT_ID, BEGIN, END
   1445         // d) Recurrence exceptions that modify an instance inside the
   1446         //   window but fall outside the window.  They have:
   1447         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS =
   1448         //   STATUS_CANCELED, EVENT_ID, BEGIN, END
   1449 
   1450         // First, delete the original instances corresponding to recurrence
   1451         // exceptions.  We do this by iterating over the list and for each
   1452         // recurrence exception, we search the list for an instance with a
   1453         // matching "original instance time".  If we find such an instance,
   1454         // we remove it from the list.  If we don't find such an instance
   1455         // then we cancel the recurrence exception.
   1456         Set<String> keys = instancesMap.keySet();
   1457         for (String syncIdKey : keys) {
   1458             InstancesList list = instancesMap.get(syncIdKey);
   1459             for (ContentValues values : list) {
   1460 
   1461                 // If this instance is not a recurrence exception, then
   1462                 // skip it.
   1463                 if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) {
   1464                     continue;
   1465                 }
   1466 
   1467                 String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR);
   1468                 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
   1469                 InstancesList originalList = instancesMap.get(originalEventPlusCalendar);
   1470                 if (originalList == null) {
   1471                     // The original recurrence is not present, so don't try canceling it.
   1472                     continue;
   1473                 }
   1474 
   1475                 // Search the original event for a matching original
   1476                 // instance time.  If there is a matching one, then remove
   1477                 // the original one.  We do this both for exceptions that
   1478                 // change the original instance as well as for exceptions
   1479                 // that delete the original instance.
   1480                 for (int num = originalList.size() - 1; num >= 0; num--) {
   1481                     ContentValues originalValues = originalList.get(num);
   1482                     long beginTime = originalValues.getAsLong(Instances.BEGIN);
   1483                     if (beginTime == originalTime) {
   1484                         // We found the original instance, so remove it.
   1485                         originalList.remove(num);
   1486                     }
   1487                 }
   1488             }
   1489         }
   1490 
   1491         // Invariant: instancesMap contains filtered instances.
   1492         // It consists of:
   1493         // a) Individual events that fall in the window.
   1494         // b) Instances of recurrences that fall in the window and have not
   1495         //   been subject to exceptions.
   1496         // c) Exceptions that fall in the window.  They will have
   1497         //   STATUS_CANCELED if they are cancellations.
   1498         // d) Recurrence exceptions that modify an instance inside the
   1499         //   window but fall outside the window.  These are STATUS_CANCELED.
   1500 
   1501         // Now do the inserts.  Since the db lock is held when this method is executed,
   1502         // this will be done in a transaction.
   1503         // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
   1504         // while the calendar app is trying to query the db (expanding instances)), we will
   1505         // not be "polite" and yield the lock until we're done.  This will favor local query
   1506         // operations over sync/write operations.
   1507         for (String syncIdKey : keys) {
   1508             InstancesList list = instancesMap.get(syncIdKey);
   1509             for (ContentValues values : list) {
   1510 
   1511                 // If this instance was cancelled or deleted then don't create a new
   1512                 // instance.
   1513                 Integer status = values.getAsInteger(Events.STATUS);
   1514                 boolean deleted = values.containsKey(Events.DELETED) ?
   1515                         values.getAsBoolean(Events.DELETED) : false;
   1516                 if ((status != null && status == Events.STATUS_CANCELED) || deleted) {
   1517                     continue;
   1518                 }
   1519 
   1520                 // We remove this useless key (not valid in the context of Instances table)
   1521                 values.remove(Events.DELETED);
   1522 
   1523                 // Remove these fields before inserting a new instance
   1524                 values.remove(ORIGINAL_EVENT_AND_CALENDAR);
   1525                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
   1526                 values.remove(Events.STATUS);
   1527 
   1528                 mDbHelper.instancesReplace(values);
   1529             }
   1530         }
   1531     }
   1532 
   1533     /**
   1534      * Computes the timezone-dependent fields of an instance of an event and
   1535      * updates the "values" map to contain those fields.
   1536      *
   1537      * @param begin the start time of the instance (in UTC milliseconds)
   1538      * @param end the end time of the instance (in UTC milliseconds)
   1539      * @param local a Time object with the timezone set to the local timezone
   1540      * @param values a map that will contain the timezone-dependent fields
   1541      */
   1542     private void computeTimezoneDependentFields(long begin, long end,
   1543             Time local, ContentValues values) {
   1544         local.set(begin);
   1545         int startDay = Time.getJulianDay(begin, local.gmtoff);
   1546         int startMinute = local.hour * 60 + local.minute;
   1547 
   1548         local.set(end);
   1549         int endDay = Time.getJulianDay(end, local.gmtoff);
   1550         int endMinute = local.hour * 60 + local.minute;
   1551 
   1552         // Special case for midnight, which has endMinute == 0.  Change
   1553         // that to +24 hours on the previous day to make everything simpler.
   1554         // Exception: if start and end minute are both 0 on the same day,
   1555         // then leave endMinute alone.
   1556         if (endMinute == 0 && endDay > startDay) {
   1557             endMinute = 24 * 60;
   1558             endDay -= 1;
   1559         }
   1560 
   1561         values.put(Instances.START_DAY, startDay);
   1562         values.put(Instances.END_DAY, endDay);
   1563         values.put(Instances.START_MINUTE, startMinute);
   1564         values.put(Instances.END_MINUTE, endMinute);
   1565     }
   1566 
   1567     @Override
   1568     public String getType(Uri url) {
   1569         int match = sUriMatcher.match(url);
   1570         switch (match) {
   1571             case EVENTS:
   1572                 return "vnd.android.cursor.dir/event";
   1573             case EVENTS_ID:
   1574                 return "vnd.android.cursor.item/event";
   1575             case REMINDERS:
   1576                 return "vnd.android.cursor.dir/reminder";
   1577             case REMINDERS_ID:
   1578                 return "vnd.android.cursor.item/reminder";
   1579             case CALENDAR_ALERTS:
   1580                 return "vnd.android.cursor.dir/calendar-alert";
   1581             case CALENDAR_ALERTS_BY_INSTANCE:
   1582                 return "vnd.android.cursor.dir/calendar-alert-by-instance";
   1583             case CALENDAR_ALERTS_ID:
   1584                 return "vnd.android.cursor.item/calendar-alert";
   1585             case INSTANCES:
   1586             case INSTANCES_BY_DAY:
   1587             case EVENT_DAYS:
   1588                 return "vnd.android.cursor.dir/event-instance";
   1589             case TIME:
   1590                 return "time/epoch";
   1591             case PROVIDER_PROPERTIES:
   1592                 return "vnd.android.cursor.dir/property";
   1593             default:
   1594                 throw new IllegalArgumentException("Unknown URL " + url);
   1595         }
   1596     }
   1597 
   1598     public static boolean isRecurrenceEvent(String rrule, String rdate, String originalEvent) {
   1599         return (!TextUtils.isEmpty(rrule)||
   1600                 !TextUtils.isEmpty(rdate)||
   1601                 !TextUtils.isEmpty(originalEvent));
   1602     }
   1603 
   1604     /**
   1605      * Takes an event and corrects the hrs, mins, secs if it is an allDay event.
   1606      *
   1607      * AllDay events should have hrs, mins, secs set to zero. This checks if this is true and
   1608      * corrects the fields DTSTART, DTEND, and DURATION if necessary. Also checks to ensure that
   1609      * either both DTSTART and DTEND or DTSTART and DURATION are set for each event.
   1610      *
   1611      * @param updatedValues The values to check and correct
   1612      * @return Returns true if a correction was necessary, false otherwise
   1613      */
   1614     private boolean fixAllDayTime(Uri uri, ContentValues updatedValues) {
   1615         boolean neededCorrection = false;
   1616         if (updatedValues.containsKey(Events.ALL_DAY)
   1617                 && updatedValues.getAsInteger(Events.ALL_DAY).intValue() == 1) {
   1618             Long dtstart = updatedValues.getAsLong(Events.DTSTART);
   1619             Long dtend = updatedValues.getAsLong(Events.DTEND);
   1620             String duration = updatedValues.getAsString(Events.DURATION);
   1621             Time time = new Time();
   1622             Cursor currentTimesCursor = null;
   1623             String tempValue;
   1624             // If a complete set of time fields doesn't exist query the db for them. A complete set
   1625             // is dtstart and dtend for non-recurring events or dtstart and duration for recurring
   1626             // events.
   1627             if(dtstart == null || (dtend == null && duration == null)) {
   1628                 // Make sure we have an id to search for, if not this is probably a new event
   1629                 if (uri.getPathSegments().size() == 2) {
   1630                     currentTimesCursor = query(uri,
   1631                             ALLDAY_TIME_PROJECTION,
   1632                             null /* selection */,
   1633                             null /* selectionArgs */,
   1634                             null /* sort */);
   1635                     if (currentTimesCursor != null) {
   1636                         if (!currentTimesCursor.moveToFirst() ||
   1637                                 currentTimesCursor.getCount() != 1) {
   1638                             // Either this is a new event or the query is too general to get data
   1639                             // from the db. In either case don't try to use the query and catch
   1640                             // errors when trying to update the time fields.
   1641                             currentTimesCursor.close();
   1642                             currentTimesCursor = null;
   1643                         }
   1644                     }
   1645                 }
   1646             }
   1647 
   1648             // Ensure dtstart exists for this event (always required) and set so h,m,s are 0 if
   1649             // necessary.
   1650             // TODO Move this somewhere to check all events, not just allDay events.
   1651             if (dtstart == null) {
   1652                 if (currentTimesCursor != null) {
   1653                     // getLong returns 0 for empty fields, we'd like to know if a field is empty
   1654                     // so getString is used instead.
   1655                     tempValue = currentTimesCursor.getString(ALLDAY_DTSTART_INDEX);
   1656                     try {
   1657                         dtstart = Long.valueOf(tempValue);
   1658                     } catch (NumberFormatException e) {
   1659                         currentTimesCursor.close();
   1660                         throw new IllegalArgumentException("Event has no DTSTART field, the db " +
   1661                             "may be damaged. Set DTSTART for this event to fix.");
   1662                     }
   1663                 } else {
   1664                     throw new IllegalArgumentException("DTSTART cannot be empty for new events.");
   1665                 }
   1666             }
   1667             time.clear(Time.TIMEZONE_UTC);
   1668             time.set(dtstart.longValue());
   1669             if (time.hour != 0 || time.minute != 0 || time.second != 0) {
   1670                 time.hour = 0;
   1671                 time.minute = 0;
   1672                 time.second = 0;
   1673                 updatedValues.put(Events.DTSTART, time.toMillis(true));
   1674                 neededCorrection = true;
   1675             }
   1676 
   1677             // If dtend exists for this event make sure it's h,m,s are 0.
   1678             if (dtend == null && currentTimesCursor != null) {
   1679                 // getLong returns 0 for empty fields. We'd like to know if a field is empty
   1680                 // so getString is used instead.
   1681                 tempValue = currentTimesCursor.getString(ALLDAY_DTEND_INDEX);
   1682                 try {
   1683                     dtend = Long.valueOf(tempValue);
   1684                 } catch (NumberFormatException e) {
   1685                     dtend = null;
   1686                 }
   1687             }
   1688             if (dtend != null) {
   1689                 time.clear(Time.TIMEZONE_UTC);
   1690                 time.set(dtend.longValue());
   1691                 if (time.hour != 0 || time.minute != 0 || time.second != 0) {
   1692                     time.hour = 0;
   1693                     time.minute = 0;
   1694                     time.second = 0;
   1695                     dtend = time.toMillis(true);
   1696                     updatedValues.put(Events.DTEND, dtend);
   1697                     neededCorrection = true;
   1698                 }
   1699             }
   1700 
   1701             if (currentTimesCursor != null) {
   1702                 if (duration == null) {
   1703                     duration = currentTimesCursor.getString(ALLDAY_DURATION_INDEX);
   1704                 }
   1705                 currentTimesCursor.close();
   1706             }
   1707 
   1708             if (duration != null) {
   1709                 int len = duration.length();
   1710                 /* duration is stored as either "P<seconds>S" or "P<days>D". This checks if it's
   1711                  * in the seconds format, and if so converts it to days.
   1712                  */
   1713                 if (len == 0) {
   1714                     duration = null;
   1715                 } else if (duration.charAt(0) == 'P' &&
   1716                         duration.charAt(len - 1) == 'S') {
   1717                     int seconds = Integer.parseInt(duration.substring(1, len - 1));
   1718                     int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
   1719                     duration = "P" + days + "D";
   1720                     updatedValues.put(Events.DURATION, duration);
   1721                     neededCorrection = true;
   1722                 } else if (duration.charAt(0) != 'P' ||
   1723                         duration.charAt(len - 1) != 'D') {
   1724                     throw new IllegalArgumentException("duration is not formatted correctly. " +
   1725                             "Should be 'P<seconds>S' or 'P<days>D'.");
   1726                 }
   1727             }
   1728 
   1729             if (duration == null && dtend == null) {
   1730                 throw new IllegalArgumentException("DTEND and DURATION cannot both be null for " +
   1731                         "an event.");
   1732             }
   1733         }
   1734         return neededCorrection;
   1735     }
   1736 
   1737     @Override
   1738     protected Uri insertInTransaction(Uri uri, ContentValues values) {
   1739         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1740             Log.v(TAG, "insertInTransaction: " + uri);
   1741         }
   1742 
   1743         final boolean callerIsSyncAdapter =
   1744                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
   1745 
   1746         final int match = sUriMatcher.match(uri);
   1747         long id = 0;
   1748 
   1749         switch (match) {
   1750               case SYNCSTATE:
   1751                 id = mDbHelper.getSyncState().insert(mDb, values);
   1752                 break;
   1753             case EVENTS:
   1754                 if (!callerIsSyncAdapter) {
   1755                     values.put(Events._SYNC_DIRTY, 1);
   1756                 }
   1757                 if (!values.containsKey(Events.DTSTART)) {
   1758                     throw new RuntimeException("DTSTART field missing from event");
   1759                 }
   1760                 // TODO: do we really need to make a copy?
   1761                 ContentValues updatedValues = new ContentValues(values);
   1762                 validateEventData(updatedValues);
   1763                 // updateLastDate must be after validation, to ensure proper last date computation
   1764                 updatedValues = updateLastDate(updatedValues);
   1765                 if (updatedValues == null) {
   1766                     throw new RuntimeException("Could not insert event.");
   1767                     // return null;
   1768                 }
   1769                 String owner = null;
   1770                 if (updatedValues.containsKey(Events.CALENDAR_ID) &&
   1771                         !updatedValues.containsKey(Events.ORGANIZER)) {
   1772                     owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
   1773                     // TODO: This isn't entirely correct.  If a guest is adding a recurrence
   1774                     // exception to an event, the organizer should stay the original organizer.
   1775                     // This value doesn't go to the server and it will get fixed on sync,
   1776                     // so it shouldn't really matter.
   1777                     if (owner != null) {
   1778                         updatedValues.put(Events.ORGANIZER, owner);
   1779                     }
   1780                 }
   1781                 if (fixAllDayTime(uri, updatedValues)) {
   1782                     if (Log.isLoggable(TAG, Log.WARN)) {
   1783                         Log.w(TAG, "insertInTransaction: " +
   1784                                 "allDay is true but sec, min, hour were not 0.");
   1785                     }
   1786                 }
   1787                 id = mDbHelper.eventsInsert(updatedValues);
   1788                 if (id != -1) {
   1789                     updateEventRawTimesLocked(id, updatedValues);
   1790                     updateInstancesLocked(updatedValues, id, true /* new event */, mDb);
   1791 
   1792                     // If we inserted a new event that specified the self-attendee
   1793                     // status, then we need to add an entry to the attendees table.
   1794                     if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
   1795                         int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS);
   1796                         if (owner == null) {
   1797                             owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
   1798                         }
   1799                         createAttendeeEntry(id, status, owner);
   1800                     }
   1801                     // if the Event Timezone is defined, store it as the original one in the
   1802                     // ExtendedProperties table
   1803                     if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) {
   1804                         String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE);
   1805 
   1806                         ContentValues expropsValues = new ContentValues();
   1807                         expropsValues.put(Calendar.ExtendedProperties.EVENT_ID, id);
   1808                         expropsValues.put(Calendar.ExtendedProperties.NAME,
   1809                                 EXT_PROP_ORIGINAL_TIMEZONE);
   1810                         expropsValues.put(Calendar.ExtendedProperties.VALUE, originalTimezone);
   1811 
   1812                         // Insert the extended property
   1813                         long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues);
   1814                         if (exPropId == -1) {
   1815                             if (Log.isLoggable(TAG, Log.ERROR)) {
   1816                                 Log.e(TAG, "Cannot add the original Timezone in the "
   1817                                         + "ExtendedProperties table for Event: " + id);
   1818                             }
   1819                         } else {
   1820                             // Update the Event for saying it has some extended properties
   1821                             ContentValues eventValues = new ContentValues();
   1822                             eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1");
   1823                             int result = mDb.update("Events", eventValues, "_id=?",
   1824                                     new String[] {String.valueOf(id)});
   1825                             if (result <= 0) {
   1826                                 if (Log.isLoggable(TAG, Log.ERROR)) {
   1827                                     Log.e(TAG, "Cannot update hasExtendedProperties column"
   1828                                             + " for Event: " + id);
   1829                                 }
   1830                             }
   1831                         }
   1832                     }
   1833                     triggerAppWidgetUpdate(id);
   1834                 }
   1835                 break;
   1836             case CALENDARS:
   1837                 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
   1838                 if (syncEvents != null && syncEvents == 1) {
   1839                     String accountName = values.getAsString(Calendars._SYNC_ACCOUNT);
   1840                     String accountType = values.getAsString(
   1841                             Calendars._SYNC_ACCOUNT_TYPE);
   1842                     final Account account = new Account(accountName, accountType);
   1843                     String calendarUrl = values.getAsString(Calendars.URL);
   1844                     mDbHelper.scheduleSync(account, false /* two-way sync */, calendarUrl);
   1845                 }
   1846                 id = mDbHelper.calendarsInsert(values);
   1847                 break;
   1848             case ATTENDEES:
   1849                 if (!values.containsKey(Attendees.EVENT_ID)) {
   1850                     throw new IllegalArgumentException("Attendees values must "
   1851                             + "contain an event_id");
   1852                 }
   1853                 id = mDbHelper.attendeesInsert(values);
   1854                 if (!callerIsSyncAdapter) {
   1855                     setEventDirty(values.getAsInteger(Attendees.EVENT_ID));
   1856                 }
   1857 
   1858                 // Copy the attendee status value to the Events table.
   1859                 updateEventAttendeeStatus(mDb, values);
   1860                 break;
   1861             case REMINDERS:
   1862                 if (!values.containsKey(Reminders.EVENT_ID)) {
   1863                     throw new IllegalArgumentException("Reminders values must "
   1864                             + "contain an event_id");
   1865                 }
   1866                 id = mDbHelper.remindersInsert(values);
   1867                 if (!callerIsSyncAdapter) {
   1868                     setEventDirty(values.getAsInteger(Reminders.EVENT_ID));
   1869                 }
   1870 
   1871                 // Schedule another event alarm, if necessary
   1872                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   1873                     Log.d(TAG, "insertInternal() changing reminder");
   1874                 }
   1875                 scheduleNextAlarm(false /* do not remove alarms */);
   1876                 break;
   1877             case CALENDAR_ALERTS:
   1878                 if (!values.containsKey(CalendarAlerts.EVENT_ID)) {
   1879                     throw new IllegalArgumentException("CalendarAlerts values must "
   1880                             + "contain an event_id");
   1881                 }
   1882                 id = mDbHelper.calendarAlertsInsert(values);
   1883                 // Note: dirty bit is not set for Alerts because it is not synced.
   1884                 // It is generated from Reminders, which is synced.
   1885                 break;
   1886             case EXTENDED_PROPERTIES:
   1887                 if (!values.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
   1888                     throw new IllegalArgumentException("ExtendedProperties values must "
   1889                             + "contain an event_id");
   1890                 }
   1891                 id = mDbHelper.extendedPropertiesInsert(values);
   1892                 if (!callerIsSyncAdapter) {
   1893                     setEventDirty(values.getAsInteger(Calendar.ExtendedProperties.EVENT_ID));
   1894                 }
   1895                 break;
   1896             case DELETED_EVENTS:
   1897             case EVENTS_ID:
   1898             case REMINDERS_ID:
   1899             case CALENDAR_ALERTS_ID:
   1900             case EXTENDED_PROPERTIES_ID:
   1901             case INSTANCES:
   1902             case INSTANCES_BY_DAY:
   1903             case EVENT_DAYS:
   1904             case PROVIDER_PROPERTIES:
   1905                 throw new UnsupportedOperationException("Cannot insert into that URL: " + uri);
   1906             default:
   1907                 throw new IllegalArgumentException("Unknown URL " + uri);
   1908         }
   1909 
   1910         if (id < 0) {
   1911             return null;
   1912         }
   1913 
   1914         return ContentUris.withAppendedId(uri, id);
   1915     }
   1916 
   1917     /**
   1918      * Do some validation on event data before inserting.
   1919      * In particular make sure dtend, duration, etc make sense for
   1920      * the type of event (regular, recurrence, exception).  Remove
   1921      * any unexpected fields.
   1922      *
   1923      * @param values the ContentValues to insert
   1924      */
   1925     private void validateEventData(ContentValues values) {
   1926         boolean hasDtend = values.getAsLong(Events.DTEND) != null;
   1927         boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION));
   1928         boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE));
   1929         boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE));
   1930         boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT));
   1931         boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null;
   1932         if (hasRrule || hasRdate) {
   1933             // Recurrence:
   1934             // dtstart is start time of first event
   1935             // dtend is null
   1936             // duration is the duration of the event
   1937             // rrule is the recurrence rule
   1938             // lastDate is the end of the last event or null if it repeats forever
   1939             // originalEvent is null
   1940             // originalInstanceTime is null
   1941             if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) {
   1942                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   1943                     Log.e(TAG, "Invalid values for recurrence: " + values);
   1944                 }
   1945                 values.remove(Events.DTEND);
   1946                 values.remove(Events.ORIGINAL_EVENT);
   1947                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
   1948             }
   1949         } else if (hasOriginalEvent || hasOriginalInstanceTime) {
   1950             // Recurrence exception
   1951             // dtstart is start time of exception event
   1952             // dtend is end time of exception event
   1953             // duration is null
   1954             // rrule is null
   1955             // lastdate is same as dtend
   1956             // originalEvent is the _sync_id of the recurrence
   1957             // originalInstanceTime is the start time of the event being replaced
   1958             if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) {
   1959                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   1960                     Log.e(TAG, "Invalid values for recurrence exception: " + values);
   1961                 }
   1962                 values.remove(Events.DURATION);
   1963             }
   1964         } else {
   1965             // Regular event
   1966             // dtstart is the start time
   1967             // dtend is the end time
   1968             // duration is null
   1969             // rrule is null
   1970             // lastDate is the same as dtend
   1971             // originalEvent is null
   1972             // originalInstanceTime is null
   1973             if (!hasDtend || hasDuration) {
   1974                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   1975                     Log.e(TAG, "Invalid values for event: " + values);
   1976                 }
   1977                 values.remove(Events.DURATION);
   1978             }
   1979         }
   1980     }
   1981 
   1982     private void setEventDirty(int eventId) {
   1983         mDb.execSQL("UPDATE Events SET _sync_dirty=1 where _id=?", new Integer[] {eventId});
   1984     }
   1985 
   1986     /**
   1987      * Gets the calendar's owner for an event.
   1988      * @param calId
   1989      * @return email of owner or null
   1990      */
   1991     private String getOwner(long calId) {
   1992         if (calId < 0) {
   1993             if (Log.isLoggable(TAG, Log.ERROR)) {
   1994                 Log.e(TAG, "Calendar Id is not valid: " + calId);
   1995             }
   1996             return null;
   1997         }
   1998         // Get the email address of this user from this Calendar
   1999         String emailAddress = null;
   2000         Cursor cursor = null;
   2001         try {
   2002             cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
   2003                     new String[] { Calendars.OWNER_ACCOUNT },
   2004                     null /* selection */,
   2005                     null /* selectionArgs */,
   2006                     null /* sort */);
   2007             if (cursor == null || !cursor.moveToFirst()) {
   2008                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   2009                     Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
   2010                 }
   2011                 return null;
   2012             }
   2013             emailAddress = cursor.getString(0);
   2014         } finally {
   2015             if (cursor != null) {
   2016                 cursor.close();
   2017             }
   2018         }
   2019         return emailAddress;
   2020     }
   2021 
   2022     /**
   2023      * Creates an entry in the Attendees table that refers to the given event
   2024      * and that has the given response status.
   2025      *
   2026      * @param eventId the event id that the new entry in the Attendees table
   2027      * should refer to
   2028      * @param status the response status
   2029      * @param emailAddress the email of the attendee
   2030      */
   2031     private void createAttendeeEntry(long eventId, int status, String emailAddress) {
   2032         ContentValues values = new ContentValues();
   2033         values.put(Attendees.EVENT_ID, eventId);
   2034         values.put(Attendees.ATTENDEE_STATUS, status);
   2035         values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
   2036         // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
   2037         // on sync.
   2038         values.put(Attendees.ATTENDEE_RELATIONSHIP,
   2039                 Attendees.RELATIONSHIP_ATTENDEE);
   2040         values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
   2041 
   2042         // We don't know the ATTENDEE_NAME but that will be filled in by the
   2043         // server and sent back to us.
   2044         mDbHelper.attendeesInsert(values);
   2045     }
   2046 
   2047     /**
   2048      * Updates the attendee status in the Events table to be consistent with
   2049      * the value in the Attendees table.
   2050      *
   2051      * @param db the database
   2052      * @param attendeeValues the column values for one row in the Attendees
   2053      * table.
   2054      */
   2055     private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
   2056         // Get the event id for this attendee
   2057         long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
   2058 
   2059         if (MULTIPLE_ATTENDEES_PER_EVENT) {
   2060             // Get the calendar id for this event
   2061             Cursor cursor = null;
   2062             long calId;
   2063             try {
   2064                 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
   2065                         new String[] { Events.CALENDAR_ID },
   2066                         null /* selection */,
   2067                         null /* selectionArgs */,
   2068                         null /* sort */);
   2069                 if (cursor == null || !cursor.moveToFirst()) {
   2070                     if (Log.isLoggable(TAG, Log.DEBUG)) {
   2071                         Log.d(TAG, "Couldn't find " + eventId + " in Events table");
   2072                     }
   2073                     return;
   2074                 }
   2075                 calId = cursor.getLong(0);
   2076             } finally {
   2077                 if (cursor != null) {
   2078                     cursor.close();
   2079                 }
   2080             }
   2081 
   2082             // Get the owner email for this Calendar
   2083             String calendarEmail = null;
   2084             cursor = null;
   2085             try {
   2086                 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
   2087                         new String[] { Calendars.OWNER_ACCOUNT },
   2088                         null /* selection */,
   2089                         null /* selectionArgs */,
   2090                         null /* sort */);
   2091                 if (cursor == null || !cursor.moveToFirst()) {
   2092                     if (Log.isLoggable(TAG, Log.DEBUG)) {
   2093                         Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
   2094                     }
   2095                     return;
   2096                 }
   2097                 calendarEmail = cursor.getString(0);
   2098             } finally {
   2099                 if (cursor != null) {
   2100                     cursor.close();
   2101                 }
   2102             }
   2103 
   2104             if (calendarEmail == null) {
   2105                 return;
   2106             }
   2107 
   2108             // Get the email address for this attendee
   2109             String attendeeEmail = null;
   2110             if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
   2111                 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
   2112             }
   2113 
   2114             // If the attendee email does not match the calendar email, then this
   2115             // attendee is not the owner of this calendar so we don't update the
   2116             // selfAttendeeStatus in the event.
   2117             if (!calendarEmail.equals(attendeeEmail)) {
   2118                 return;
   2119             }
   2120         }
   2121 
   2122         int status = Attendees.ATTENDEE_STATUS_NONE;
   2123         if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
   2124             int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
   2125             if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
   2126                 status = Attendees.ATTENDEE_STATUS_ACCEPTED;
   2127             }
   2128         }
   2129 
   2130         if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
   2131             status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
   2132         }
   2133 
   2134         ContentValues values = new ContentValues();
   2135         values.put(Events.SELF_ATTENDEE_STATUS, status);
   2136         db.update("Events", values, "_id=?", new String[] {String.valueOf(eventId)});
   2137     }
   2138 
   2139     /**
   2140      * Updates the instances table when an event is added or updated.
   2141      * @param values The new values of the event.
   2142      * @param rowId The database row id of the event.
   2143      * @param newEvent true if the event is new.
   2144      * @param db The database
   2145      */
   2146     private void updateInstancesLocked(ContentValues values,
   2147             long rowId,
   2148             boolean newEvent,
   2149             SQLiteDatabase db) {
   2150 
   2151         // If there are no expanded Instances, then return.
   2152         MetaData.Fields fields = mMetaData.getFieldsLocked();
   2153         if (fields.maxInstance == 0) {
   2154             return;
   2155         }
   2156 
   2157         Long dtstartMillis = values.getAsLong(Events.DTSTART);
   2158         if (dtstartMillis == null) {
   2159             if (newEvent) {
   2160                 // must be present for a new event.
   2161                 throw new RuntimeException("DTSTART missing.");
   2162             }
   2163             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   2164                 Log.v(TAG, "Missing DTSTART.  No need to update instance.");
   2165             }
   2166             return;
   2167         }
   2168 
   2169         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
   2170         Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
   2171 
   2172         if (!newEvent) {
   2173             // Want to do this for regular event, recurrence, or exception.
   2174             // For recurrence or exception, more deletion may happen below if we
   2175             // do an instance expansion.  This deletion will suffice if the exception
   2176             // is moved outside the window, for instance.
   2177             db.delete("Instances", "event_id=?", new String[] {String.valueOf(rowId)});
   2178         }
   2179 
   2180         String rrule = values.getAsString(Events.RRULE);
   2181         String rdate = values.getAsString(Events.RDATE);
   2182         String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
   2183         if (isRecurrenceEvent(rrule, rdate, originalEvent))  {
   2184             // The recurrence or exception needs to be (re-)expanded if:
   2185             // a) Exception or recurrence that falls inside window
   2186             boolean insideWindow = dtstartMillis <= fields.maxInstance &&
   2187                     (lastDateMillis == null || lastDateMillis >= fields.minInstance);
   2188             // b) Exception that affects instance inside window
   2189             // These conditions match the query in getEntries
   2190             //  See getEntries comment for explanation of subtracting 1 week.
   2191             boolean affectsWindow = originalInstanceTime != null &&
   2192                     originalInstanceTime <= fields.maxInstance &&
   2193                     originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
   2194             if (insideWindow || affectsWindow) {
   2195                 updateRecurrenceInstancesLocked(values, rowId, db);
   2196             }
   2197             // TODO: an exception creation or update could be optimized by
   2198             // updating just the affected instances, instead of regenerating
   2199             // the recurrence.
   2200             return;
   2201         }
   2202 
   2203         Long dtendMillis = values.getAsLong(Events.DTEND);
   2204         if (dtendMillis == null) {
   2205             dtendMillis = dtstartMillis;
   2206         }
   2207 
   2208         // if the event is in the expanded range, insert
   2209         // into the instances table.
   2210         // TODO: deal with durations.  currently, durations are only used in
   2211         // recurrences.
   2212 
   2213         if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
   2214             ContentValues instanceValues = new ContentValues();
   2215             instanceValues.put(Instances.EVENT_ID, rowId);
   2216             instanceValues.put(Instances.BEGIN, dtstartMillis);
   2217             instanceValues.put(Instances.END, dtendMillis);
   2218 
   2219             boolean allDay = false;
   2220             Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
   2221             if (allDayInteger != null) {
   2222                 allDay = allDayInteger != 0;
   2223             }
   2224 
   2225             // Update the timezone-dependent fields.
   2226             Time local = new Time();
   2227             if (allDay) {
   2228                 local.timezone = Time.TIMEZONE_UTC;
   2229             } else {
   2230                 local.timezone = fields.timezone;
   2231             }
   2232 
   2233             computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
   2234             mDbHelper.instancesInsert(instanceValues);
   2235         }
   2236     }
   2237 
   2238     /**
   2239      * Determines the recurrence entries associated with a particular recurrence.
   2240      * This set is the base recurrence and any exception.
   2241      *
   2242      * Normally the entries are indicated by the sync id of the base recurrence
   2243      * (which is the originalEvent in the exceptions).
   2244      * However, a complication is that a recurrence may not yet have a sync id.
   2245      * In that case, the recurrence is specified by the rowId.
   2246      *
   2247      * @param recurrenceSyncId The sync id of the base recurrence, or null.
   2248      * @param rowId The row id of the base recurrence.
   2249      * @return the relevant entries.
   2250      */
   2251     private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
   2252         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   2253 
   2254         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
   2255         qb.setProjectionMap(sEventsProjectionMap);
   2256         String selectionArgs[];
   2257         if (recurrenceSyncId == null) {
   2258             String where = "_id =?";
   2259             qb.appendWhere(where);
   2260             selectionArgs = new String[] {String.valueOf(rowId)};
   2261         } else {
   2262             String where = "_sync_id = ? OR originalEvent = ?";
   2263             qb.appendWhere(where);
   2264             selectionArgs = new String[] {recurrenceSyncId, recurrenceSyncId};
   2265         }
   2266         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   2267             Log.v(TAG, "Retrieving events to expand: " + qb.toString());
   2268         }
   2269 
   2270         return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs,
   2271                 null /* groupBy */, null /* having */, null /* sortOrder */);
   2272     }
   2273 
   2274     /**
   2275      * Do incremental Instances update of a recurrence or recurrence exception.
   2276      *
   2277      * This method does performInstanceExpansion on just the modified recurrence,
   2278      * to avoid the overhead of recomputing the entire instance table.
   2279      *
   2280      * @param values The new values of the event.
   2281      * @param rowId The database row id of the event.
   2282      * @param db The database
   2283      */
   2284     private void updateRecurrenceInstancesLocked(ContentValues values,
   2285             long rowId,
   2286             SQLiteDatabase db) {
   2287         MetaData.Fields fields = mMetaData.getFieldsLocked();
   2288         String instancesTimezone = mCalendarCache.readTimezoneInstances();
   2289         String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
   2290         String recurrenceSyncId;
   2291         if (originalEvent != null) {
   2292             recurrenceSyncId = originalEvent;
   2293         } else {
   2294             // Get the recurrence's sync id from the database
   2295             recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
   2296                     + " WHERE _id=?", new String[] {String.valueOf(rowId)});
   2297         }
   2298         // recurrenceSyncId is the _sync_id of the underlying recurrence
   2299         // If the recurrence hasn't gone to the server, it will be null.
   2300 
   2301         // Need to clear out old instances
   2302         if (recurrenceSyncId == null) {
   2303             // Creating updating a recurrence that hasn't gone to the server.
   2304             // Need to delete based on row id
   2305             String where = "_id IN (SELECT Instances._id as _id"
   2306                     + " FROM Instances INNER JOIN Events"
   2307                     + " ON (Events._id = Instances.event_id)"
   2308                     + " WHERE Events._id =?)";
   2309             db.delete("Instances", where, new String[]{"" + rowId});
   2310         } else {
   2311             // Creating or modifying a recurrence or exception.
   2312             // Delete instances for recurrence (_sync_id = recurrenceSyncId)
   2313             // and all exceptions (originalEvent = recurrenceSyncId)
   2314             String where = "_id IN (SELECT Instances._id as _id"
   2315                     + " FROM Instances INNER JOIN Events"
   2316                     + " ON (Events._id = Instances.event_id)"
   2317                     + " WHERE Events._sync_id =?"
   2318                     + " OR Events.originalEvent =?)";
   2319             db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
   2320         }
   2321 
   2322         // Now do instance expansion
   2323         Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
   2324         try {
   2325             performInstanceExpansion(fields.minInstance, fields.maxInstance, instancesTimezone,
   2326                                      entries);
   2327         } finally {
   2328             if (entries != null) {
   2329                 entries.close();
   2330             }
   2331         }
   2332     }
   2333 
   2334     long calculateLastDate(ContentValues values)
   2335             throws DateException {
   2336         // Allow updates to some event fields like the title or hasAlarm
   2337         // without requiring DTSTART.
   2338         if (!values.containsKey(Events.DTSTART)) {
   2339             if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
   2340                     || values.containsKey(Events.DURATION)
   2341                     || values.containsKey(Events.EVENT_TIMEZONE)
   2342                     || values.containsKey(Events.RDATE)
   2343                     || values.containsKey(Events.EXRULE)
   2344                     || values.containsKey(Events.EXDATE)) {
   2345                 throw new RuntimeException("DTSTART field missing from event");
   2346             }
   2347             return -1;
   2348         }
   2349         long dtstartMillis = values.getAsLong(Events.DTSTART);
   2350         long lastMillis = -1;
   2351 
   2352         // Can we use dtend with a repeating event?  What does that even
   2353         // mean?
   2354         // NOTE: if the repeating event has a dtend, we convert it to a
   2355         // duration during event processing, so this situation should not
   2356         // occur.
   2357         Long dtEnd = values.getAsLong(Events.DTEND);
   2358         if (dtEnd != null) {
   2359             lastMillis = dtEnd;
   2360         } else {
   2361             // find out how long it is
   2362             Duration duration = new Duration();
   2363             String durationStr = values.getAsString(Events.DURATION);
   2364             if (durationStr != null) {
   2365                 duration.parse(durationStr);
   2366             }
   2367 
   2368             RecurrenceSet recur = null;
   2369             try {
   2370                 recur = new RecurrenceSet(values);
   2371             } catch (EventRecurrence.InvalidFormatException e) {
   2372                 if (Log.isLoggable(TAG, Log.WARN)) {
   2373                     Log.w(TAG, "Could not parse RRULE recurrence string: " +
   2374                             values.get(Calendar.Events.RRULE), e);
   2375                 }
   2376                 return lastMillis; // -1
   2377             }
   2378 
   2379             if (null != recur && recur.hasRecurrence()) {
   2380                 // the event is repeating, so find the last date it
   2381                 // could appear on
   2382 
   2383                 String tz = values.getAsString(Events.EVENT_TIMEZONE);
   2384 
   2385                 if (TextUtils.isEmpty(tz)) {
   2386                     // floating timezone
   2387                     tz = Time.TIMEZONE_UTC;
   2388                 }
   2389                 Time dtstartLocal = new Time(tz);
   2390 
   2391                 dtstartLocal.set(dtstartMillis);
   2392 
   2393                 RecurrenceProcessor rp = new RecurrenceProcessor();
   2394                 lastMillis = rp.getLastOccurence(dtstartLocal, recur);
   2395                 if (lastMillis == -1) {
   2396                     return lastMillis;  // -1
   2397                 }
   2398             } else {
   2399                 // the event is not repeating, just use dtstartMillis
   2400                 lastMillis = dtstartMillis;
   2401             }
   2402 
   2403             // that was the beginning of the event.  this is the end.
   2404             lastMillis = duration.addTo(lastMillis);
   2405         }
   2406         return lastMillis;
   2407     }
   2408 
   2409     /**
   2410      * Add LAST_DATE to values.
   2411      * @param values the ContentValues (in/out)
   2412      * @return values on success, null on failure
   2413      */
   2414     private ContentValues updateLastDate(ContentValues values) {
   2415         try {
   2416             long last = calculateLastDate(values);
   2417             if (last != -1) {
   2418                 values.put(Events.LAST_DATE, last);
   2419             }
   2420 
   2421             return values;
   2422         } catch (DateException e) {
   2423             // don't add it if there was an error
   2424             if (Log.isLoggable(TAG, Log.WARN)) {
   2425                 Log.w(TAG, "Could not calculate last date.", e);
   2426             }
   2427             return null;
   2428         }
   2429     }
   2430 
   2431     private void updateEventRawTimesLocked(long eventId, ContentValues values) {
   2432         ContentValues rawValues = new ContentValues();
   2433 
   2434         rawValues.put("event_id", eventId);
   2435 
   2436         String timezone = values.getAsString(Events.EVENT_TIMEZONE);
   2437 
   2438         boolean allDay = false;
   2439         Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
   2440         if (allDayInteger != null) {
   2441             allDay = allDayInteger != 0;
   2442         }
   2443 
   2444         if (allDay || TextUtils.isEmpty(timezone)) {
   2445             // floating timezone
   2446             timezone = Time.TIMEZONE_UTC;
   2447         }
   2448 
   2449         Time time = new Time(timezone);
   2450         time.allDay = allDay;
   2451         Long dtstartMillis = values.getAsLong(Events.DTSTART);
   2452         if (dtstartMillis != null) {
   2453             time.set(dtstartMillis);
   2454             rawValues.put("dtstart2445", time.format2445());
   2455         }
   2456 
   2457         Long dtendMillis = values.getAsLong(Events.DTEND);
   2458         if (dtendMillis != null) {
   2459             time.set(dtendMillis);
   2460             rawValues.put("dtend2445", time.format2445());
   2461         }
   2462 
   2463         Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
   2464         if (originalInstanceMillis != null) {
   2465             // This is a recurrence exception so we need to get the all-day
   2466             // status of the original recurring event in order to format the
   2467             // date correctly.
   2468             allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
   2469             if (allDayInteger != null) {
   2470                 time.allDay = allDayInteger != 0;
   2471             }
   2472             time.set(originalInstanceMillis);
   2473             rawValues.put("originalInstanceTime2445", time.format2445());
   2474         }
   2475 
   2476         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
   2477         if (lastDateMillis != null) {
   2478             time.allDay = allDay;
   2479             time.set(lastDateMillis);
   2480             rawValues.put("lastDate2445", time.format2445());
   2481         }
   2482 
   2483         mDbHelper.eventsRawTimesReplace(rawValues);
   2484     }
   2485 
   2486     @Override
   2487     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
   2488         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   2489             Log.v(TAG, "deleteInTransaction: " + uri);
   2490         }
   2491         final boolean callerIsSyncAdapter =
   2492                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
   2493         final int match = sUriMatcher.match(uri);
   2494         switch (match) {
   2495             case SYNCSTATE:
   2496                 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
   2497 
   2498             case SYNCSTATE_ID:
   2499                 String selectionWithId = (BaseColumns._ID + "=?")
   2500                         + (selection == null ? "" : " AND (" + selection + ")");
   2501                 // Prepend id to selectionArgs
   2502                 selectionArgs = insertSelectionArg(selectionArgs,
   2503                         String.valueOf(ContentUris.parseId(uri)));
   2504                 return mDbHelper.getSyncState().delete(mDb, selectionWithId,
   2505                         selectionArgs);
   2506 
   2507             case EVENTS:
   2508             {
   2509                 int result = 0;
   2510                 selection = appendAccountToSelection(uri, selection);
   2511 
   2512                 // Query this event to get the ids to delete.
   2513                 Cursor cursor = mDb.query("Events", ID_ONLY_PROJECTION,
   2514                         selection, selectionArgs, null /* groupBy */,
   2515                         null /* having */, null /* sortOrder */);
   2516                 try {
   2517                     while (cursor.moveToNext()) {
   2518                         long id = cursor.getLong(0);
   2519                         result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
   2520                     }
   2521                     scheduleNextAlarm(false /* do not remove alarms */);
   2522                     triggerAppWidgetUpdate(-1 /* changedEventId */);
   2523                 } finally {
   2524                     cursor.close();
   2525                     cursor = null;
   2526                 }
   2527                 return result;
   2528             }
   2529             case EVENTS_ID:
   2530             {
   2531                 long id = ContentUris.parseId(uri);
   2532                 if (selection != null) {
   2533                     throw new UnsupportedOperationException("CalendarProvider2 "
   2534                             + "doesn't support selection based deletion for type "
   2535                             + match);
   2536                 }
   2537                 return deleteEventInternal(id, callerIsSyncAdapter, false /* isBatch */);
   2538             }
   2539             case ATTENDEES:
   2540             {
   2541                 if (callerIsSyncAdapter) {
   2542                     return mDb.delete("Attendees", selection, selectionArgs);
   2543                 } else {
   2544                     return deleteFromTable("Attendees", uri, selection, selectionArgs);
   2545                 }
   2546             }
   2547             case ATTENDEES_ID:
   2548             {
   2549                 if (selection != null) {
   2550                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   2551                 }
   2552                 if (callerIsSyncAdapter) {
   2553                     long id = ContentUris.parseId(uri);
   2554                     return mDb.delete("Attendees", "_id=?", new String[] {String.valueOf(id)});
   2555                 } else {
   2556                     return deleteFromTable("Attendees", uri, null /* selection */,
   2557                                            null /* selectionArgs */);
   2558                 }
   2559             }
   2560             case REMINDERS:
   2561             {
   2562                 if (callerIsSyncAdapter) {
   2563                     return mDb.delete("Reminders", selection, selectionArgs);
   2564                 } else {
   2565                     return deleteFromTable("Reminders", uri, selection, selectionArgs);
   2566                 }
   2567             }
   2568             case REMINDERS_ID:
   2569             {
   2570                 if (selection != null) {
   2571                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   2572                 }
   2573                 if (callerIsSyncAdapter) {
   2574                     long id = ContentUris.parseId(uri);
   2575                     return mDb.delete("Reminders", "_id=?", new String[] {String.valueOf(id)});
   2576                 } else {
   2577                     return deleteFromTable("Reminders", uri, null /* selection */,
   2578                                            null /* selectionArgs */);
   2579                 }
   2580             }
   2581             case EXTENDED_PROPERTIES:
   2582             {
   2583                 if (callerIsSyncAdapter) {
   2584                     return mDb.delete("ExtendedProperties", selection, selectionArgs);
   2585                 } else {
   2586                     return deleteFromTable("ExtendedProperties", uri, selection, selectionArgs);
   2587                 }
   2588             }
   2589             case EXTENDED_PROPERTIES_ID:
   2590             {
   2591                 if (selection != null) {
   2592                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   2593                 }
   2594                 if (callerIsSyncAdapter) {
   2595                     long id = ContentUris.parseId(uri);
   2596                     return mDb.delete("ExtendedProperties", "_id=?",
   2597                             new String[] {String.valueOf(id)});
   2598                 } else {
   2599                     return deleteFromTable("ExtendedProperties", uri, null /* selection */,
   2600                                            null /* selectionArgs */);
   2601                 }
   2602             }
   2603             case CALENDAR_ALERTS:
   2604             {
   2605                 if (callerIsSyncAdapter) {
   2606                     return mDb.delete("CalendarAlerts", selection, selectionArgs);
   2607                 } else {
   2608                     return deleteFromTable("CalendarAlerts", uri, selection, selectionArgs);
   2609                 }
   2610             }
   2611             case CALENDAR_ALERTS_ID:
   2612             {
   2613                 if (selection != null) {
   2614                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   2615                 }
   2616                 // Note: dirty bit is not set for Alerts because it is not synced.
   2617                 // It is generated from Reminders, which is synced.
   2618                 long id = ContentUris.parseId(uri);
   2619                 return mDb.delete("CalendarAlerts", "_id=?", new String[] {String.valueOf(id)});
   2620             }
   2621             case DELETED_EVENTS:
   2622                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
   2623             case CALENDARS_ID:
   2624                 StringBuilder selectionSb = new StringBuilder("_id=");
   2625                 selectionSb.append(uri.getPathSegments().get(1));
   2626                 if (!TextUtils.isEmpty(selection)) {
   2627                     selectionSb.append(" AND (");
   2628                     selectionSb.append(selection);
   2629                     selectionSb.append(')');
   2630                 }
   2631                 selection = selectionSb.toString();
   2632                 // fall through to CALENDARS for the actual delete
   2633             case CALENDARS:
   2634                 selection = appendAccountToSelection(uri, selection);
   2635                 return deleteMatchingCalendars(selection); // TODO: handle in sync adapter
   2636             case INSTANCES:
   2637             case INSTANCES_BY_DAY:
   2638             case EVENT_DAYS:
   2639             case PROVIDER_PROPERTIES:
   2640                 throw new UnsupportedOperationException("Cannot delete that URL");
   2641             default:
   2642                 throw new IllegalArgumentException("Unknown URL " + uri);
   2643         }
   2644     }
   2645 
   2646     private int deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch) {
   2647         int result = 0;
   2648         String selectionArgs[] = new String[] {String.valueOf(id)};
   2649 
   2650         // Query this event to get the fields needed for deleting.
   2651         Cursor cursor = mDb.query("Events", EVENTS_PROJECTION,
   2652                 "_id=?", selectionArgs,
   2653                 null /* groupBy */,
   2654                 null /* having */, null /* sortOrder */);
   2655         try {
   2656             if (cursor.moveToNext()) {
   2657                 result = 1;
   2658                 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
   2659                 boolean emptySyncId = TextUtils.isEmpty(syncId);
   2660 
   2661                 // If this was a recurring event or a recurrence
   2662                 // exception, then force a recalculation of the
   2663                 // instances.
   2664                 String rrule = cursor.getString(EVENTS_RRULE_INDEX);
   2665                 String rdate = cursor.getString(EVENTS_RDATE_INDEX);
   2666                 String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
   2667                 if (isRecurrenceEvent(rrule, rdate, origEvent)) {
   2668                     mMetaData.clearInstanceRange();
   2669                 }
   2670 
   2671                 // we clean the Events and Attendees table if the caller is CalendarSyncAdapter
   2672                 // or if the event is local (no syncId)
   2673                 if (callerIsSyncAdapter || emptySyncId) {
   2674                     mDb.delete("Events", "_id=?", selectionArgs);
   2675                 } else {
   2676                     ContentValues values = new ContentValues();
   2677                     values.put(Events.DELETED, 1);
   2678                     values.put(Events._SYNC_DIRTY, 1);
   2679                     mDb.update("Events", values, "_id=?", selectionArgs);
   2680 
   2681                     // Delete associated data; attendees, however, are deleted with the actual event
   2682                     // so that the sync adapter is able to notify attendees of the cancellation.
   2683                     mDb.delete("Instances", "event_id=?", selectionArgs);
   2684                     mDb.delete("EventsRawTimes", "event_id=?", selectionArgs);
   2685                     mDb.delete("Reminders", "event_id=?", selectionArgs);
   2686                     mDb.delete("CalendarAlerts", "event_id=?", selectionArgs);
   2687                     mDb.delete("ExtendedProperties", "event_id=?", selectionArgs);
   2688                 }
   2689             }
   2690         } finally {
   2691             cursor.close();
   2692             cursor = null;
   2693         }
   2694 
   2695         if (!isBatch) {
   2696             scheduleNextAlarm(false /* do not remove alarms */);
   2697             triggerAppWidgetUpdate(-1 /* changedEventId */);
   2698         }
   2699 
   2700         return result;
   2701     }
   2702 
   2703     /**
   2704      * Delete rows from a table and mark corresponding events as dirty.
   2705      * @param table The table to delete from
   2706      * @param uri The URI specifying the rows
   2707      * @param selection for the query
   2708      * @param selectionArgs for the query
   2709      */
   2710     private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) {
   2711         // Note that the query will return data according to the access restrictions,
   2712         // so we don't need to worry about deleting data we don't have permission to read.
   2713         Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
   2714         ContentValues values = new ContentValues();
   2715         values.put(Events._SYNC_DIRTY, "1");
   2716         int count = 0;
   2717         try {
   2718             while(c.moveToNext()) {
   2719                 long id = c.getLong(ID_INDEX);
   2720                 long event_id = c.getLong(EVENT_ID_INDEX);
   2721                 mDb.delete(table, "_id=?", new String[] {String.valueOf(id)});
   2722                 mDb.update("Events", values, "_id=?", new String[] {String.valueOf(event_id)});
   2723                 count++;
   2724             }
   2725         } finally {
   2726             c.close();
   2727         }
   2728         return count;
   2729     }
   2730 
   2731     /**
   2732      * Update rows in a table and mark corresponding events as dirty.
   2733      * @param table The table to delete from
   2734      * @param values The values to update
   2735      * @param uri The URI specifying the rows
   2736      * @param selection for the query
   2737      * @param selectionArgs for the query
   2738      */
   2739     private int updateInTable(String table, ContentValues values, Uri uri, String selection,
   2740             String[] selectionArgs) {
   2741         // Note that the query will return data according to the access restrictions,
   2742         // so we don't need to worry about deleting data we don't have permission to read.
   2743         Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
   2744         ContentValues dirtyValues = new ContentValues();
   2745         dirtyValues.put(Events._SYNC_DIRTY, "1");
   2746         int count = 0;
   2747         try {
   2748             while(c.moveToNext()) {
   2749                 long id = c.getLong(ID_INDEX);
   2750                 long event_id = c.getLong(EVENT_ID_INDEX);
   2751                 mDb.update(table, values, "_id=?", new String[] {String.valueOf(id)});
   2752                 mDb.update("Events", dirtyValues, "_id=?", new String[] {String.valueOf(event_id)});
   2753                 count++;
   2754             }
   2755         } finally {
   2756             c.close();
   2757         }
   2758         return count;
   2759     }
   2760 
   2761     private int deleteMatchingCalendars(String where) {
   2762         // query to find all the calendars that match, for each
   2763         // - delete calendar subscription
   2764         // - delete calendar
   2765 
   2766         Cursor c = mDb.query("Calendars", sCalendarsIdProjection, where,
   2767                 null /* selectionArgs */, null /* groupBy */,
   2768                 null /* having */, null /* sortOrder */);
   2769         if (c == null) {
   2770             return 0;
   2771         }
   2772         try {
   2773             while (c.moveToNext()) {
   2774                 long id = c.getLong(CALENDARS_INDEX_ID);
   2775                 modifyCalendarSubscription(id, false /* not selected */);
   2776             }
   2777         } finally {
   2778             c.close();
   2779         }
   2780         return mDb.delete("Calendars", where, null /* whereArgs */);
   2781     }
   2782 
   2783     private Cursor getCursorForEventIdAndProjection(String eventId, String[] projection) {
   2784         return mDb.query(Tables.EVENTS,
   2785                 projection,
   2786                 SQL_WHERE_ID,
   2787                 new String[] { eventId },
   2788                 null /* group by */,
   2789                 null /* having */,
   2790                 null /* order by*/);
   2791     }
   2792 
   2793     private boolean doesEventExistForSyncId(String syncId) {
   2794         if (syncId == null) {
   2795             if (Log.isLoggable(TAG, Log.WARN)) {
   2796                 Log.w(TAG, "SyncID cannot be null: " + syncId);
   2797             }
   2798             return false;
   2799         }
   2800         long count = DatabaseUtils.longForQuery(mDb, SQL_SELECT_COUNT_FOR_SYNC_ID,
   2801                 new String[] { syncId });
   2802         return (count > 0);
   2803     }
   2804 
   2805     // Check if an UPDATE with STATUS_CANCEL means that we will need to do an Update (instead of
   2806     // a Deletion)
   2807     //
   2808     // Deletion will be done only and only if:
   2809     // - event status = canceled
   2810     // - event is a recurrence exception that does not have its original (parent) event anymore
   2811     //
   2812     // This is due to the Server semantics that generate STATUS_CANCELED for both creation
   2813     // and deletion of a recurrence exception
   2814     // See bug #3218104
   2815     private boolean doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values) {
   2816         boolean isStatusCanceled = values.containsKey(Events.STATUS) &&
   2817                 (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED);
   2818         if (isStatusCanceled) {
   2819             Cursor cursor = null;
   2820             try {
   2821                 cursor = getCursorForEventIdAndProjection(eventId,
   2822                         new String[] { Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT });
   2823                 if (!cursor.moveToFirst()) {
   2824                     if (Log.isLoggable(TAG, Log.WARN)) {
   2825                         Log.w(TAG, "Cannot find Event with id: " + eventId);
   2826                     }
   2827                     return false;
   2828                 }
   2829                 String rrule = cursor.getString(0);
   2830                 String rdate = cursor.getString(1);
   2831                 String originalEvent = cursor.getString(2);
   2832 
   2833                 boolean isRecurrenceException =
   2834                         isRecurrenceEvent(rrule, rdate, originalEvent) &&
   2835                         !TextUtils.isEmpty(originalEvent);
   2836 
   2837                 if (isRecurrenceException) {
   2838                     return doesEventExistForSyncId(originalEvent);
   2839                 }
   2840             } finally {
   2841                 cursor.close();
   2842             }
   2843         }
   2844         // This is the normal case, we just want an UPDATE
   2845         return true;
   2846     }
   2847 
   2848     // TODO: call calculateLastDate()!
   2849     @Override
   2850     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
   2851             String[] selectionArgs) {
   2852         if (Log.isLoggable(TAG, Log.VERBOSE)) {
   2853             Log.v(TAG, "updateInTransaction: " + uri);
   2854         }
   2855 
   2856         int count = 0;
   2857 
   2858         final int match = sUriMatcher.match(uri);
   2859 
   2860         final boolean callerIsSyncAdapter =
   2861                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
   2862 
   2863         // TODO: remove this restriction
   2864         if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS
   2865                 && match != EVENTS && match != PROVIDER_PROPERTIES) {
   2866             throw new IllegalArgumentException(
   2867                     "WHERE based updates not supported");
   2868         }
   2869         switch (match) {
   2870             case SYNCSTATE:
   2871                 return mDbHelper.getSyncState().update(mDb, values,
   2872                         appendAccountToSelection(uri, selection), selectionArgs);
   2873 
   2874             case SYNCSTATE_ID: {
   2875                 selection = appendAccountToSelection(uri, selection);
   2876                 String selectionWithId = (BaseColumns._ID + "=?")
   2877                         + (selection == null ? "" : " AND (" + selection + ")");
   2878                 // Prepend id to selectionArgs
   2879                 selectionArgs = insertSelectionArg(selectionArgs,
   2880                         String.valueOf(ContentUris.parseId(uri)));
   2881                 return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs);
   2882             }
   2883 
   2884             case CALENDARS_ID:
   2885             {
   2886                 if (selection != null) {
   2887                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   2888                 }
   2889                 long id = ContentUris.parseId(uri);
   2890                 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
   2891                 if (syncEvents != null) {
   2892                     modifyCalendarSubscription(id, syncEvents == 1);
   2893                 }
   2894 
   2895                 int result = mDb.update("Calendars", values, "_id=?",
   2896                         new String[] {String.valueOf(id)});
   2897 
   2898                 // The calendar should not be displayed in widget either.
   2899                 final Integer selected = values.getAsInteger(Calendars.SELECTED);
   2900                 if (selected != null && selected == 0) {
   2901                     triggerAppWidgetUpdate(-1);
   2902                 }
   2903 
   2904                 return result;
   2905             }
   2906             case EVENTS:
   2907             case EVENTS_ID:
   2908             {
   2909                 long id = 0;
   2910                 if (match == EVENTS_ID) {
   2911                     id = ContentUris.parseId(uri);
   2912                 } else if (callerIsSyncAdapter) {
   2913                     if (selection != null && selection.startsWith("_id=")) {
   2914                         // The ContentProviderOperation generates an _id=n string instead of
   2915                         // adding the id to the URL, so parse that out here.
   2916                         id = Long.parseLong(selection.substring(4));
   2917                     } else {
   2918                         // Sync adapter Events operation affects just Events table, not associated
   2919                         // tables.
   2920                         if (fixAllDayTime(uri, values)) {
   2921                             if (Log.isLoggable(TAG, Log.WARN)) {
   2922                                 Log.w(TAG, "updateInTransaction: Caller is sync adapter. " +
   2923                                         "allDay is true but sec, min, hour were not 0.");
   2924                             }
   2925                         }
   2926                         return mDb.update("Events", values, selection, selectionArgs);
   2927                     }
   2928                 } else {
   2929                     throw new IllegalArgumentException("Unknown URL " + uri);
   2930                 }
   2931                 if (!callerIsSyncAdapter) {
   2932                     values.put(Events._SYNC_DIRTY, 1);
   2933                 }
   2934                 // Disallow updating the attendee status in the Events
   2935                 // table.  In the future, we could support this but we
   2936                 // would have to query and update the attendees table
   2937                 // to keep the values consistent.
   2938                 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
   2939                     throw new IllegalArgumentException("Updating "
   2940                             + Events.SELF_ATTENDEE_STATUS
   2941                             + " in Events table is not allowed.");
   2942                 }
   2943 
   2944                 // TODO: should we allow this?
   2945                 if (values.containsKey(Events.HTML_URI) && !callerIsSyncAdapter) {
   2946                     throw new IllegalArgumentException("Updating "
   2947                             + Events.HTML_URI
   2948                             + " in Events table is not allowed.");
   2949                 }
   2950                 String strId = String.valueOf(id);
   2951                 // For taking care about recurrences exceptions cancelations, check if this needs
   2952                 //  to be an UPDATE or a DELETE
   2953                 boolean isUpdate = doesStatusCancelUpdateMeanUpdate(strId, values);
   2954                 ContentValues updatedValues = new ContentValues(values);
   2955                 // TODO: should extend validateEventData to work with updates and call it here
   2956                 updatedValues = updateLastDate(updatedValues);
   2957                 if (updatedValues == null) {
   2958                     if (Log.isLoggable(TAG, Log.WARN)) {
   2959                         Log.w(TAG, "Could not update event.");
   2960                     }
   2961                     return 0;
   2962                 }
   2963                 // Make sure we pass in a uri with the id appended to fixAllDayTime
   2964                 Uri allDayUri;
   2965                 if (uri.getPathSegments().size() == 1) {
   2966                     allDayUri = ContentUris.withAppendedId(uri, id);
   2967                 } else {
   2968                     allDayUri = uri;
   2969                 }
   2970                 if (fixAllDayTime(allDayUri, updatedValues)) {
   2971                     if (Log.isLoggable(TAG, Log.WARN)) {
   2972                         Log.w(TAG, "updateInTransaction: " +
   2973                                 "allDay is true but sec, min, hour were not 0.");
   2974                     }
   2975                 }
   2976 
   2977                 int result;
   2978 
   2979                 if (isUpdate) {
   2980                     result = mDb.update("Events", updatedValues, "_id=?",
   2981                             new String[] {String.valueOf(id)});
   2982                     if (result > 0) {
   2983                         updateEventRawTimesLocked(id, updatedValues);
   2984                         updateInstancesLocked(updatedValues, id, false /* not a new event */, mDb);
   2985 
   2986                         if (values.containsKey(Events.DTSTART)) {
   2987                             // The start time of the event changed, so run the
   2988                             // event alarm scheduler.
   2989                             if (Log.isLoggable(TAG, Log.DEBUG)) {
   2990                                 Log.d(TAG, "updateInternal() changing event");
   2991                             }
   2992                             scheduleNextAlarm(false /* do not remove alarms */);
   2993                             triggerAppWidgetUpdate(id);
   2994                         }
   2995                     }
   2996                 } else {
   2997                     result = deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
   2998                     scheduleNextAlarm(false /* do not remove alarms */);
   2999                     triggerAppWidgetUpdate(id);
   3000                 }
   3001                 return result;
   3002             }
   3003             case ATTENDEES_ID: {
   3004                 if (selection != null) {
   3005                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   3006                 }
   3007                 // Copy the attendee status value to the Events table.
   3008                 updateEventAttendeeStatus(mDb, values);
   3009 
   3010                 if (callerIsSyncAdapter) {
   3011                     long id = ContentUris.parseId(uri);
   3012                     return mDb.update("Attendees", values, "_id=?",
   3013                             new String[] {String.valueOf(id)});
   3014                 } else {
   3015                     return updateInTable("Attendees", values, uri, null /* selection */,
   3016                             null /* selectionArgs */);
   3017                 }
   3018             }
   3019             case CALENDAR_ALERTS_ID: {
   3020                 if (selection != null) {
   3021                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   3022                 }
   3023                 // Note: dirty bit is not set for Alerts because it is not synced.
   3024                 // It is generated from Reminders, which is synced.
   3025                 long id = ContentUris.parseId(uri);
   3026                 return mDb.update("CalendarAlerts", values, "_id=?",
   3027                         new String[] {String.valueOf(id)});
   3028             }
   3029             case CALENDAR_ALERTS: {
   3030                 // Note: dirty bit is not set for Alerts because it is not synced.
   3031                 // It is generated from Reminders, which is synced.
   3032                 return mDb.update("CalendarAlerts", values, selection, selectionArgs);
   3033             }
   3034             case REMINDERS_ID: {
   3035                 if (selection != null) {
   3036                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   3037                 }
   3038                 if (callerIsSyncAdapter) {
   3039                     long id = ContentUris.parseId(uri);
   3040                     count = mDb.update("Reminders", values, "_id=?",
   3041                             new String[] {String.valueOf(id)});
   3042                 } else {
   3043                     count = updateInTable("Reminders", values, uri, null /* selection */,
   3044                             null /* selectionArgs */);
   3045                 }
   3046 
   3047                 // Reschedule the event alarms because the
   3048                 // "minutes" field may have changed.
   3049                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   3050                     Log.d(TAG, "updateInternal() changing reminder");
   3051                 }
   3052                 scheduleNextAlarm(false /* do not remove alarms */);
   3053                 return count;
   3054             }
   3055             case EXTENDED_PROPERTIES_ID: {
   3056                 if (selection != null) {
   3057                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
   3058                 }
   3059                 if (callerIsSyncAdapter) {
   3060                     long id = ContentUris.parseId(uri);
   3061                     return mDb.update("ExtendedProperties", values, "_id=?",
   3062                             new String[] {String.valueOf(id)});
   3063                 } else {
   3064                     return updateInTable("ExtendedProperties", values, uri, null /* selection */,
   3065                             null /* selectionArgs */);
   3066                 }
   3067             }
   3068             // TODO: replace the SCHEDULE_ALARM private URIs with a
   3069             // service
   3070             case SCHEDULE_ALARM: {
   3071                 scheduleNextAlarm(false);
   3072                 return 0;
   3073             }
   3074             case SCHEDULE_ALARM_REMOVE: {
   3075                 scheduleNextAlarm(true);
   3076                 return 0;
   3077             }
   3078 
   3079             case PROVIDER_PROPERTIES: {
   3080                 if (selection == null) {
   3081                     throw new UnsupportedOperationException("Selection cannot be null for " + uri);
   3082                 }
   3083                 if (!selection.equals("key=?")) {
   3084                     throw new UnsupportedOperationException("Selection should be key=? for " + uri);
   3085                 }
   3086 
   3087                 List<String> list = Arrays.asList(selectionArgs);
   3088 
   3089                 if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) {
   3090                     throw new UnsupportedOperationException("Invalid selection key: " +
   3091                             CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + " for " + uri);
   3092                 }
   3093 
   3094                 // Before it may be changed, save current Instances timezone for later use
   3095                 String timezoneInstancesBeforeUpdate = mCalendarCache.readTimezoneInstances();
   3096 
   3097                 // Update the database with the provided values (this call may change the value
   3098                 // of timezone Instances)
   3099                 int result = mDb.update("CalendarCache", values, selection, selectionArgs);
   3100 
   3101                 // if successful, do some house cleaning:
   3102                 // if the timezone type is set to "home", set the Instances timezone to the previous
   3103                 // if the timezone type is set to "auto", set the Instances timezone to the current
   3104                 //      device one
   3105                 // if the timezone Instances is set AND if we are in "home" timezone type, then
   3106                 //      save the timezone Instance into "previous" too
   3107                 if (result > 0) {
   3108                     // If we are changing timezone type...
   3109                     if (list.contains(CalendarCache.KEY_TIMEZONE_TYPE)) {
   3110                         String value = values.getAsString(CalendarCache.COLUMN_NAME_VALUE);
   3111                         if (value != null) {
   3112                             // if we are setting timezone type to "home"
   3113                             if (value.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
   3114                                 String previousTimezone =
   3115                                         mCalendarCache.readTimezoneInstancesPrevious();
   3116                                 if (previousTimezone != null) {
   3117                                     mCalendarCache.writeTimezoneInstances(previousTimezone);
   3118                                 }
   3119                                 // Regenerate Instances if the "home" timezone has changed
   3120                                 if (!timezoneInstancesBeforeUpdate.equals(previousTimezone) ) {
   3121                                     regenerateInstancesTable();
   3122                                 }
   3123                             }
   3124                             // if we are setting timezone type to "auto"
   3125                             else if (value.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
   3126                                 String localTimezone = TimeZone.getDefault().getID();
   3127                                 mCalendarCache.writeTimezoneInstances(localTimezone);
   3128                                 if (!timezoneInstancesBeforeUpdate.equals(localTimezone)) {
   3129                                     regenerateInstancesTable();
   3130                                 }
   3131                             }
   3132                         }
   3133                     }
   3134                     // If we are changing timezone Instances...
   3135                     else if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES)) {
   3136                         // if we are in "home" timezone type...
   3137                         if (isHomeTimezone()) {
   3138                             String timezoneInstances = mCalendarCache.readTimezoneInstances();
   3139                             // Update the previous value
   3140                             mCalendarCache.writeTimezoneInstancesPrevious(timezoneInstances);
   3141                             // Recompute Instances if the "home" timezone has changed
   3142                             if (timezoneInstancesBeforeUpdate != null &&
   3143                                     !timezoneInstancesBeforeUpdate.equals(timezoneInstances)) {
   3144                                 regenerateInstancesTable();
   3145                             }
   3146                         }
   3147                     }
   3148                     triggerAppWidgetUpdate(-1);
   3149                 }
   3150                 return result;
   3151             }
   3152 
   3153             default:
   3154                 throw new IllegalArgumentException("Unknown URL " + uri);
   3155         }
   3156     }
   3157 
   3158     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
   3159         final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
   3160         final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
   3161         if (!TextUtils.isEmpty(accountName)) {
   3162             qb.appendWhere(Calendar.Calendars._SYNC_ACCOUNT + "="
   3163                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   3164                     + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
   3165                     + DatabaseUtils.sqlEscapeString(accountType));
   3166         } else {
   3167             qb.appendWhere("1"); // I.e. always true
   3168         }
   3169     }
   3170 
   3171     private String appendAccountToSelection(Uri uri, String selection) {
   3172         final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
   3173         final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
   3174         if (!TextUtils.isEmpty(accountName)) {
   3175             StringBuilder selectionSb = new StringBuilder(Calendar.Calendars._SYNC_ACCOUNT + "="
   3176                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   3177                     + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
   3178                     + DatabaseUtils.sqlEscapeString(accountType));
   3179             if (!TextUtils.isEmpty(selection)) {
   3180                 selectionSb.append(" AND (");
   3181                 selectionSb.append(selection);
   3182                 selectionSb.append(')');
   3183             }
   3184             return selectionSb.toString();
   3185         } else {
   3186             return selection;
   3187         }
   3188     }
   3189 
   3190     private void modifyCalendarSubscription(long id, boolean syncEvents) {
   3191         // get the account, url, and current selected state
   3192         // for this calendar.
   3193         Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
   3194                 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
   3195                         Calendars.URL, Calendars.SYNC_EVENTS},
   3196                 null /* selection */,
   3197                 null /* selectionArgs */,
   3198                 null /* sort */);
   3199 
   3200         Account account = null;
   3201         String calendarUrl = null;
   3202         boolean oldSyncEvents = false;
   3203         if (cursor != null) {
   3204             try {
   3205                 if (cursor.moveToFirst()) {
   3206                     final String accountName = cursor.getString(0);
   3207                     final String accountType = cursor.getString(1);
   3208                     account = new Account(accountName, accountType);
   3209                     calendarUrl = cursor.getString(2);
   3210                     oldSyncEvents = (cursor.getInt(3) != 0);
   3211                 }
   3212             } finally {
   3213                 cursor.close();
   3214             }
   3215         }
   3216 
   3217         if (account == null) {
   3218             // should not happen?
   3219             if (Log.isLoggable(TAG, Log.WARN)) {
   3220                 Log.w(TAG, "Cannot update subscription because account "
   3221                         + "is empty -- should not happen.");
   3222             }
   3223             return;
   3224         }
   3225 
   3226         if (TextUtils.isEmpty(calendarUrl)) {
   3227             // Passing in a null Url will cause it to not add any extras
   3228             // Should only happen for non-google calendars.
   3229             calendarUrl = null;
   3230         }
   3231 
   3232         if (oldSyncEvents == syncEvents) {
   3233             // nothing to do
   3234             return;
   3235         }
   3236 
   3237         // If the calendar is not selected for syncing, then don't download
   3238         // events.
   3239         mDbHelper.scheduleSync(account, !syncEvents, calendarUrl);
   3240     }
   3241 
   3242     // TODO: is this needed
   3243 //    @Override
   3244 //    public void onSyncStop(SyncContext context, boolean success) {
   3245 //        super.onSyncStop(context, success);
   3246 //        if (Log.isLoggable(TAG, Log.DEBUG)) {
   3247 //            Log.d(TAG, "onSyncStop() success: " + success);
   3248 //        }
   3249 //        scheduleNextAlarm(false /* do not remove alarms */);
   3250 //        triggerAppWidgetUpdate(-1);
   3251 //    }
   3252 
   3253     /**
   3254      * Update any existing widgets with the changed events.
   3255      *
   3256      * @param changedEventId Specific event known to be changed, otherwise -1.
   3257      *            If present, we use it to decide if an update is necessary.
   3258      */
   3259     private synchronized void triggerAppWidgetUpdate(long changedEventId) {
   3260         Context context = getContext();
   3261         if (context != null) {
   3262             mAppWidgetProvider.providerUpdated(context, changedEventId);
   3263         }
   3264     }
   3265 
   3266     /* Retrieve and cache the alarm manager */
   3267     private AlarmManager getAlarmManager() {
   3268         synchronized(mAlarmLock) {
   3269             if (mAlarmManager == null) {
   3270                 Context context = getContext();
   3271                 if (context == null) {
   3272                     if (Log.isLoggable(TAG, Log.ERROR)) {
   3273                         Log.e(TAG, "getAlarmManager() cannot get Context");
   3274                     }
   3275                     return null;
   3276                 }
   3277                 Object service = context.getSystemService(Context.ALARM_SERVICE);
   3278                 mAlarmManager = (AlarmManager) service;
   3279             }
   3280             return mAlarmManager;
   3281         }
   3282     }
   3283 
   3284     void scheduleNextAlarmCheck(long triggerTime) {
   3285         AlarmManager manager = getAlarmManager();
   3286         if (manager == null) {
   3287             if (Log.isLoggable(TAG, Log.ERROR)) {
   3288                 Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
   3289             }
   3290             return;
   3291         }
   3292         Context context = getContext();
   3293         Intent intent = new Intent(CalendarReceiver.SCHEDULE);
   3294         intent.setClass(context, CalendarReceiver.class);
   3295         PendingIntent pending = PendingIntent.getBroadcast(context,
   3296                 0, intent, PendingIntent.FLAG_NO_CREATE);
   3297         if (pending != null) {
   3298             // Cancel any previous alarms that do the same thing.
   3299             manager.cancel(pending);
   3300         }
   3301         pending = PendingIntent.getBroadcast(context,
   3302                 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
   3303 
   3304         if (Log.isLoggable(TAG, Log.DEBUG)) {
   3305             Time time = new Time();
   3306             time.set(triggerTime);
   3307             String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
   3308             Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
   3309         }
   3310 
   3311         manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
   3312     }
   3313 
   3314     /*
   3315      * This method runs the alarm scheduler in a background thread.
   3316      */
   3317     void scheduleNextAlarm(boolean removeAlarms) {
   3318         synchronized (mAlarmLock) {
   3319             if (mAlarmScheduler == null) {
   3320                 mAlarmScheduler = new AlarmScheduler(removeAlarms);
   3321                 mAlarmScheduler.start();
   3322             } else {
   3323                 mRerunAlarmScheduler = true;
   3324                 // removing the alarms is a stronger action so it has
   3325                 // precedence.
   3326                 mRemoveAlarmsOnRerun = mRemoveAlarmsOnRerun || removeAlarms;
   3327             }
   3328         }
   3329     }
   3330 
   3331     /**
   3332      * This method runs in a background thread and schedules an alarm for
   3333      * the next calendar event, if necessary.
   3334      */
   3335     private void runScheduleNextAlarm(boolean removeAlarms) {
   3336         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
   3337         db.beginTransaction();
   3338         try {
   3339             if (removeAlarms) {
   3340                 removeScheduledAlarmsLocked(db);
   3341             }
   3342             scheduleNextAlarmLocked(db);
   3343             db.setTransactionSuccessful();
   3344         } finally {
   3345             db.endTransaction();
   3346         }
   3347     }
   3348 
   3349     /**
   3350      * This method looks at the 24-hour window from now for any events that it
   3351      * needs to schedule.  This method runs within a database transaction. It
   3352      * also runs in a background thread.
   3353      *
   3354      * The CalendarProvider2 keeps track of which alarms it has already scheduled
   3355      * to avoid scheduling them more than once and for debugging problems with
   3356      * alarms.  It stores this knowledge in a database table called CalendarAlerts
   3357      * which persists across reboots.  But the actual alarm list is in memory
   3358      * and disappears if the phone loses power.  To avoid missing an alarm, we
   3359      * clear the entries in the CalendarAlerts table when we start up the
   3360      * CalendarProvider2.
   3361      *
   3362      * Scheduling an alarm multiple times is not tragic -- we filter out the
   3363      * extra ones when we receive them. But we still need to keep track of the
   3364      * scheduled alarms. The main reason is that we need to prevent multiple
   3365      * notifications for the same alarm (on the receive side) in case we
   3366      * accidentally schedule the same alarm multiple times.  We don't have
   3367      * visibility into the system's alarm list so we can never know for sure if
   3368      * we have already scheduled an alarm and it's better to err on scheduling
   3369      * an alarm twice rather than missing an alarm.  Another reason we keep
   3370      * track of scheduled alarms in a database table is that it makes it easy to
   3371      * run an SQL query to find the next reminder that we haven't scheduled.
   3372      *
   3373      * @param db the database
   3374      */
   3375     private void scheduleNextAlarmLocked(SQLiteDatabase db) {
   3376         AlarmManager alarmManager = getAlarmManager();
   3377         if (alarmManager == null) {
   3378             if (Log.isLoggable(TAG, Log.ERROR)) {
   3379                 Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
   3380             }
   3381             return;
   3382         }
   3383 
   3384         final long currentMillis = System.currentTimeMillis();
   3385         final long start = currentMillis - SCHEDULE_ALARM_SLACK;
   3386         final long end = start + (24 * 60 * 60 * 1000);
   3387         ContentResolver cr = getContext().getContentResolver();
   3388         if (Log.isLoggable(TAG, Log.DEBUG)) {
   3389             Time time = new Time();
   3390             time.set(start);
   3391             String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
   3392             Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
   3393         }
   3394 
   3395         // Delete rows in CalendarAlert where the corresponding Instance or
   3396         // Reminder no longer exist.
   3397         // Also clear old alarms but keep alarms around for a while to prevent
   3398         // multiple alerts for the same reminder.  The "clearUpToTime'
   3399         // should be further in the past than the point in time where
   3400         // we start searching for events (the "start" variable defined above).
   3401         String selectArg[] = new String[] {
   3402             Long.toString(currentMillis - CLEAR_OLD_ALARM_THRESHOLD)
   3403         };
   3404 
   3405         int rowsDeleted =
   3406             db.delete(CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg);
   3407 
   3408         long nextAlarmTime = end;
   3409         final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
   3410         if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) {
   3411             nextAlarmTime = tmpAlarmTime;
   3412         }
   3413 
   3414         // Extract events from the database sorted by alarm time.  The
   3415         // alarm times are computed from Instances.begin (whose units
   3416         // are milliseconds) and Reminders.minutes (whose units are
   3417         // minutes).
   3418         //
   3419         // Also, ignore events whose end time is already in the past.
   3420         // Also, ignore events alarms that we have already scheduled.
   3421         //
   3422         // Note 1: we can add support for the case where Reminders.minutes
   3423         // equals -1 to mean use Calendars.minutes by adding a UNION for
   3424         // that case where the two halves restrict the WHERE clause on
   3425         // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
   3426         //
   3427         // Note 2: we have to name "myAlarmTime" different from the
   3428         // "alarmTime" column in CalendarAlerts because otherwise the
   3429         // query won't find multiple alarms for the same event.
   3430         //
   3431         // The CAST is needed in the query because otherwise the expression
   3432         // will be untyped and sqlite3's manifest typing will not convert the
   3433         // string query parameter to an int in myAlarmtime>=?, so the comparison
   3434         // will fail.  This could be simplified if bug 2464440 is resolved.
   3435         String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
   3436                 + " Instances.event_id AS eventId, begin, end,"
   3437                 + " title, allDay, method, minutes"
   3438                 + " FROM Instances INNER JOIN Events"
   3439                 + " ON (Events._id = Instances.event_id)"
   3440                 + " INNER JOIN Reminders"
   3441                 + " ON (Instances.event_id = Reminders.event_id)"
   3442                 + " WHERE method=" + Reminders.METHOD_ALERT
   3443                 + " AND myAlarmTime>=CAST(? AS INT)"
   3444                 + " AND myAlarmTime<=CAST(? AS INT)"
   3445                 + " AND end>=?"
   3446                 + " AND 0=(SELECT count(*) from CalendarAlerts CA"
   3447                 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
   3448                 + " AND CA.alarmTime=myAlarmTime)"
   3449                 + " ORDER BY myAlarmTime,begin,title";
   3450         String queryParams[] = new String[] {String.valueOf(start), String.valueOf(nextAlarmTime),
   3451                 String.valueOf(currentMillis)};
   3452 
   3453         String instancesTimezone = mCalendarCache.readTimezoneInstances();
   3454         boolean isHomeTimezone = mCalendarCache.readTimezoneType().equals(
   3455                 CalendarCache.TIMEZONE_TYPE_HOME);
   3456         acquireInstanceRangeLocked(start,
   3457                 end,
   3458                 false /* don't use minimum expansion windows */,
   3459                 false /* do not force Instances deletion and expansion */,
   3460                 instancesTimezone,
   3461                 isHomeTimezone);
   3462         Cursor cursor = null;
   3463         try {
   3464             cursor = db.rawQuery(query, queryParams);
   3465 
   3466             final int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
   3467             final int endIndex = cursor.getColumnIndex(Instances.END);
   3468             final int eventIdIndex = cursor.getColumnIndex("eventId");
   3469             final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
   3470             final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
   3471 
   3472             if (Log.isLoggable(TAG, Log.DEBUG)) {
   3473                 Time time = new Time();
   3474                 time.set(nextAlarmTime);
   3475                 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
   3476                 Log.d(TAG, "cursor results: " + cursor.getCount() + " nextAlarmTime: "
   3477                         + alarmTimeStr);
   3478             }
   3479 
   3480             while (cursor.moveToNext()) {
   3481                 // Schedule all alarms whose alarm time is as early as any
   3482                 // scheduled alarm.  For example, if the earliest alarm is at
   3483                 // 1pm, then we will schedule all alarms that occur at 1pm
   3484                 // but no alarms that occur later than 1pm.
   3485                 // Actually, we allow alarms up to a minute later to also
   3486                 // be scheduled so that we don't have to check immediately
   3487                 // again after an event alarm goes off.
   3488                 final long alarmTime = cursor.getLong(alarmTimeIndex);
   3489                 final long eventId = cursor.getLong(eventIdIndex);
   3490                 final int minutes = cursor.getInt(minutesIndex);
   3491                 final long startTime = cursor.getLong(beginIndex);
   3492                 final long endTime = cursor.getLong(endIndex);
   3493 
   3494                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   3495                     Time time = new Time();
   3496                     time.set(alarmTime);
   3497                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
   3498                     time.set(startTime);
   3499                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
   3500 
   3501                     Log.d(TAG, "  looking at id: " + eventId + " " + startTime + startTimeStr
   3502                             + " alarm: " + alarmTime + schedTime);
   3503                 }
   3504 
   3505                 if (alarmTime < nextAlarmTime) {
   3506                     nextAlarmTime = alarmTime;
   3507                 } else if (alarmTime >
   3508                            nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) {
   3509                     // This event alarm (and all later ones) will be scheduled
   3510                     // later.
   3511                     if (Log.isLoggable(TAG, Log.DEBUG)) {
   3512                         Log.d(TAG, "This event alarm (and all later ones) will be scheduled later");
   3513                     }
   3514                     break;
   3515                 }
   3516 
   3517                 // Avoid an SQLiteContraintException by checking if this alarm
   3518                 // already exists in the table.
   3519                 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
   3520                     if (Log.isLoggable(TAG, Log.DEBUG)) {
   3521                         int titleIndex = cursor.getColumnIndex(Events.TITLE);
   3522                         String title = cursor.getString(titleIndex);
   3523                         Log.d(TAG, "  alarm exists for id: " + eventId + " " + title);
   3524                     }
   3525                     continue;
   3526                 }
   3527 
   3528                 // Insert this alarm into the CalendarAlerts table
   3529                 Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
   3530                         endTime, alarmTime, minutes);
   3531                 if (uri == null) {
   3532                     if (Log.isLoggable(TAG, Log.ERROR)) {
   3533                         Log.e(TAG, "runScheduleNextAlarm() insert into "
   3534                                 + "CalendarAlerts table failed");
   3535                     }
   3536                     continue;
   3537                 }
   3538 
   3539                 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, alarmTime);
   3540             }
   3541         } finally {
   3542             if (cursor != null) {
   3543                 cursor.close();
   3544             }
   3545         }
   3546 
   3547         // Refresh notification bar
   3548         if (rowsDeleted > 0) {
   3549             CalendarAlerts.scheduleAlarm(getContext(), alarmManager, currentMillis);
   3550         }
   3551 
   3552         // If we scheduled an event alarm, then schedule the next alarm check
   3553         // for one minute past that alarm.  Otherwise, if there were no
   3554         // event alarms scheduled, then check again in 24 hours.  If a new
   3555         // event is inserted before the next alarm check, then this method
   3556         // will be run again when the new event is inserted.
   3557         if (nextAlarmTime != Long.MAX_VALUE) {
   3558             scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS);
   3559         } else {
   3560             scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS);
   3561         }
   3562     }
   3563 
   3564     /**
   3565      * Removes the entries in the CalendarAlerts table for alarms that we have
   3566      * scheduled but that have not fired yet. We do this to ensure that we
   3567      * don't miss an alarm.  The CalendarAlerts table keeps track of the
   3568      * alarms that we have scheduled but the actual alarm list is in memory
   3569      * and will be cleared if the phone reboots.
   3570      *
   3571      * We don't need to remove entries that have already fired, and in fact
   3572      * we should not remove them because we need to display the notifications
   3573      * until the user dismisses them.
   3574      *
   3575      * We could remove entries that have fired and been dismissed, but we leave
   3576      * them around for a while because it makes it easier to debug problems.
   3577      * Entries that are old enough will be cleaned up later when we schedule
   3578      * new alarms.
   3579      */
   3580     private void removeScheduledAlarmsLocked(SQLiteDatabase db) {
   3581         if (Log.isLoggable(TAG, Log.DEBUG)) {
   3582             Log.d(TAG, "removing scheduled alarms");
   3583         }
   3584         db.delete(CalendarAlerts.TABLE_NAME,
   3585                 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
   3586     }
   3587 
   3588     private static String sEventsTable = "Events";
   3589     private static String sAttendeesTable = "Attendees";
   3590     private static String sRemindersTable = "Reminders";
   3591     private static String sCalendarAlertsTable = "CalendarAlerts";
   3592     private static String sExtendedPropertiesTable = "ExtendedProperties";
   3593 
   3594     private static final int EVENTS = 1;
   3595     private static final int EVENTS_ID = 2;
   3596     private static final int INSTANCES = 3;
   3597     private static final int DELETED_EVENTS = 4;
   3598     private static final int CALENDARS = 5;
   3599     private static final int CALENDARS_ID = 6;
   3600     private static final int ATTENDEES = 7;
   3601     private static final int ATTENDEES_ID = 8;
   3602     private static final int REMINDERS = 9;
   3603     private static final int REMINDERS_ID = 10;
   3604     private static final int EXTENDED_PROPERTIES = 11;
   3605     private static final int EXTENDED_PROPERTIES_ID = 12;
   3606     private static final int CALENDAR_ALERTS = 13;
   3607     private static final int CALENDAR_ALERTS_ID = 14;
   3608     private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
   3609     private static final int INSTANCES_BY_DAY = 16;
   3610     private static final int SYNCSTATE = 17;
   3611     private static final int SYNCSTATE_ID = 18;
   3612     private static final int EVENT_ENTITIES = 19;
   3613     private static final int EVENT_ENTITIES_ID = 20;
   3614     private static final int EVENT_DAYS = 21;
   3615     private static final int SCHEDULE_ALARM = 22;
   3616     private static final int SCHEDULE_ALARM_REMOVE = 23;
   3617     private static final int TIME = 24;
   3618     private static final int PROVIDER_PROPERTIES = 25;
   3619 
   3620     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   3621     private static final HashMap<String, String> sInstancesProjectionMap;
   3622     private static final HashMap<String, String> sEventsProjectionMap;
   3623     private static final HashMap<String, String> sEventEntitiesProjectionMap;
   3624     private static final HashMap<String, String> sAttendeesProjectionMap;
   3625     private static final HashMap<String, String> sRemindersProjectionMap;
   3626     private static final HashMap<String, String> sCalendarAlertsProjectionMap;
   3627     private static final HashMap<String, String> sCalendarCacheProjectionMap;
   3628 
   3629     static {
   3630         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES);
   3631         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY);
   3632         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS);
   3633         sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS);
   3634         sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID);
   3635         sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES);
   3636         sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID);
   3637         sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS);
   3638         sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID);
   3639         sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS);
   3640         sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES);
   3641         sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID);
   3642         sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS);
   3643         sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID);
   3644         sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES);
   3645         sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID);
   3646         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS);
   3647         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID);
   3648         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance",
   3649                            CALENDAR_ALERTS_BY_INSTANCE);
   3650         sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE);
   3651         sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
   3652         sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM);
   3653         sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE);
   3654         sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME);
   3655         sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME);
   3656         sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES);
   3657 
   3658         sEventsProjectionMap = new HashMap<String, String>();
   3659         // Events columns
   3660         sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
   3661         sEventsProjectionMap.put(Events.TITLE, "title");
   3662         sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
   3663         sEventsProjectionMap.put(Events.DESCRIPTION, "description");
   3664         sEventsProjectionMap.put(Events.STATUS, "eventStatus");
   3665         sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
   3666         sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
   3667         sEventsProjectionMap.put(Events.DTSTART, "dtstart");
   3668         sEventsProjectionMap.put(Events.DTEND, "dtend");
   3669         sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
   3670         sEventsProjectionMap.put(Events.DURATION, "duration");
   3671         sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
   3672         sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
   3673         sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
   3674         sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
   3675         sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
   3676         sEventsProjectionMap.put(Events.RRULE, "rrule");
   3677         sEventsProjectionMap.put(Events.RDATE, "rdate");
   3678         sEventsProjectionMap.put(Events.EXRULE, "exrule");
   3679         sEventsProjectionMap.put(Events.EXDATE, "exdate");
   3680         sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
   3681         sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
   3682         sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
   3683         sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
   3684         sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
   3685         sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
   3686         sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
   3687         sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
   3688         sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
   3689         sEventsProjectionMap.put(Events.ORGANIZER, "organizer");
   3690         sEventsProjectionMap.put(Events.DELETED, "deleted");
   3691 
   3692         // Put the shared items into the Attendees, Reminders projection map
   3693         sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
   3694         sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
   3695 
   3696         // Calendar columns
   3697         sEventsProjectionMap.put(Calendars.COLOR, "color");
   3698         sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level");
   3699         sEventsProjectionMap.put(Calendars.SELECTED, "selected");
   3700         sEventsProjectionMap.put(Calendars.URL, "url");
   3701         sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
   3702         sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount");
   3703 
   3704         // Put the shared items into the Instances projection map
   3705         // The Instances and CalendarAlerts are joined with Calendars, so the projections include
   3706         // the above Calendar columns.
   3707         sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
   3708         sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
   3709 
   3710         sEventsProjectionMap.put(Events._ID, "_id");
   3711         sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id");
   3712         sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version");
   3713         sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time");
   3714         sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id");
   3715         sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty");
   3716         sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account");
   3717         sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
   3718                 "_sync_account_type");
   3719 
   3720         sEventEntitiesProjectionMap = new HashMap<String, String>();
   3721         sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri");
   3722         sEventEntitiesProjectionMap.put(Events.TITLE, "title");
   3723         sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description");
   3724         sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
   3725         sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus");
   3726         sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
   3727         sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
   3728         sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart");
   3729         sEventEntitiesProjectionMap.put(Events.DTEND, "dtend");
   3730         sEventEntitiesProjectionMap.put(Events.DURATION, "duration");
   3731         sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
   3732         sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay");
   3733         sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility");
   3734         sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency");
   3735         sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
   3736         sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
   3737         sEventEntitiesProjectionMap.put(Events.RRULE, "rrule");
   3738         sEventEntitiesProjectionMap.put(Events.RDATE, "rdate");
   3739         sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule");
   3740         sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate");
   3741         sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
   3742         sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
   3743         sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
   3744         sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate");
   3745         sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
   3746         sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
   3747         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
   3748         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
   3749         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
   3750         sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer");
   3751         sEventEntitiesProjectionMap.put(Events.DELETED, "deleted");
   3752         sEventEntitiesProjectionMap.put(Events._ID, Events._ID);
   3753         sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID);
   3754         sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA);
   3755         sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION);
   3756         sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY);
   3757         sEventEntitiesProjectionMap.put(Calendars.URL, "url");
   3758 
   3759         // Instances columns
   3760         sInstancesProjectionMap.put(Instances.BEGIN, "begin");
   3761         sInstancesProjectionMap.put(Instances.END, "end");
   3762         sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
   3763         sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
   3764         sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
   3765         sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
   3766         sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
   3767         sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
   3768 
   3769         // Attendees columns
   3770         sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
   3771         sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
   3772         sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
   3773         sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
   3774         sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
   3775         sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
   3776         sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
   3777 
   3778         // Reminders columns
   3779         sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
   3780         sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
   3781         sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
   3782         sRemindersProjectionMap.put(Reminders.METHOD, "method");
   3783 
   3784         // CalendarAlerts columns
   3785         sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
   3786         sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
   3787         sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
   3788         sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
   3789         sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
   3790         sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
   3791         sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
   3792 
   3793         // CalendarCache columns
   3794         sCalendarCacheProjectionMap = new HashMap<String, String>();
   3795         sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key");
   3796         sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value");
   3797     }
   3798 
   3799     /**
   3800      * Make sure that there are no entries for accounts that no longer
   3801      * exist. We are overriding this since we need to delete from the
   3802      * Calendars table, which is not syncable, which has triggers that
   3803      * will delete from the Events and  tables, which are
   3804      * syncable.  TODO: update comment, make sure deletes don't get synced.
   3805      */
   3806     public void onAccountsUpdated(Account[] accounts) {
   3807         mDb = mDbHelper.getWritableDatabase();
   3808         if (mDb == null) return;
   3809 
   3810         HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>();
   3811         HashSet<Account> validAccounts = new HashSet<Account>();
   3812         for (Account account : accounts) {
   3813             validAccounts.add(new Account(account.name, account.type));
   3814             accountHasCalendar.put(account, false);
   3815         }
   3816         ArrayList<Account> accountsToDelete = new ArrayList<Account>();
   3817 
   3818         mDb.beginTransaction();
   3819         try {
   3820 
   3821             for (String table : new String[]{"Calendars"}) {
   3822                 // Find all the accounts the contacts DB knows about, mark the ones that aren't
   3823                 // in the valid set for deletion.
   3824                 Cursor c = mDb.rawQuery("SELECT DISTINCT " + CalendarDatabaseHelper.ACCOUNT_NAME
   3825                                         + ","
   3826                                         + CalendarDatabaseHelper.ACCOUNT_TYPE + " from "
   3827                         + table, null);
   3828                 while (c.moveToNext()) {
   3829                     if (c.getString(0) != null && c.getString(1) != null) {
   3830                         Account currAccount = new Account(c.getString(0), c.getString(1));
   3831                         if (!validAccounts.contains(currAccount)) {
   3832                             accountsToDelete.add(currAccount);
   3833                         }
   3834                     }
   3835                 }
   3836                 c.close();
   3837             }
   3838 
   3839             for (Account account : accountsToDelete) {
   3840                 if (Log.isLoggable(TAG, Log.DEBUG)) {
   3841                     Log.d(TAG, "removing data for removed account " + account);
   3842                 }
   3843                 String[] params = new String[]{account.name, account.type};
   3844                 mDb.execSQL("DELETE FROM Calendars"
   3845                         + " WHERE " + CalendarDatabaseHelper.ACCOUNT_NAME + "= ? AND "
   3846                         + CalendarDatabaseHelper.ACCOUNT_TYPE
   3847                         + "= ?", params);
   3848             }
   3849             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
   3850             mDb.setTransactionSuccessful();
   3851         } finally {
   3852             mDb.endTransaction();
   3853         }
   3854     }
   3855 
   3856     /* package */ static boolean readBooleanQueryParameter(Uri uri, String name,
   3857             boolean defaultValue) {
   3858         final String flag = getQueryParameter(uri, name);
   3859         return flag == null
   3860                 ? defaultValue
   3861                 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
   3862     }
   3863 
   3864     // Duplicated from ContactsProvider2.  TODO: a utility class for shared code
   3865     /**
   3866      * A fast re-implementation of {@link Uri#getQueryParameter}
   3867      */
   3868     /* package */ static String getQueryParameter(Uri uri, String parameter) {
   3869         String query = uri.getEncodedQuery();
   3870         if (query == null) {
   3871             return null;
   3872         }
   3873 
   3874         int queryLength = query.length();
   3875         int parameterLength = parameter.length();
   3876 
   3877         String value;
   3878         int index = 0;
   3879         while (true) {
   3880             index = query.indexOf(parameter, index);
   3881             if (index == -1) {
   3882                 return null;
   3883             }
   3884 
   3885             index += parameterLength;
   3886 
   3887             if (queryLength == index) {
   3888                 return null;
   3889             }
   3890 
   3891             if (query.charAt(index) == '=') {
   3892                 index++;
   3893                 break;
   3894             }
   3895         }
   3896 
   3897         int ampIndex = query.indexOf('&', index);
   3898         if (ampIndex == -1) {
   3899             value = query.substring(index);
   3900         } else {
   3901             value = query.substring(index, ampIndex);
   3902         }
   3903 
   3904         return Uri.decode(value);
   3905     }
   3906 
   3907     /**
   3908      * Inserts an argument at the beginning of the selection arg list.
   3909      *
   3910      * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is
   3911      * prepended to the user's where clause (combined with 'AND') to generate
   3912      * the final where close, so arguments associated with the QueryBuilder are
   3913      * prepended before any user selection args to keep them in the right order.
   3914      */
   3915     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
   3916         if (selectionArgs == null) {
   3917             return new String[] {arg};
   3918         } else {
   3919             int newLength = selectionArgs.length + 1;
   3920             String[] newSelectionArgs = new String[newLength];
   3921             newSelectionArgs[0] = arg;
   3922             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
   3923             return newSelectionArgs;
   3924         }
   3925     }
   3926 }
   3927