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