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