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