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