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