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