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