CalendarProvider2.java revision 85c09a31bcc3a18e173428bf7b628cec2834bebc
1/*
2**
3** Copyright 2006, The Android Open Source Project
4**
5** Licensed under the Apache License, Version 2.0 (the "License");
6** you may not use this file except in compliance with the License.
7** You may obtain a copy of the License at
8**
9**     http://www.apache.org/licenses/LICENSE-2.0
10**
11** Unless required by applicable law or agreed to in writing, software
12** distributed under the License is distributed on an "AS IS" BASIS,
13** See the License for the specific language governing permissions and
14** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15** limitations under the License.
16*/
17
18package com.android.providers.calendar;
19
20import com.android.calendarcommon.DateException;
21import com.android.calendarcommon.EventRecurrence;
22import com.android.calendarcommon.RecurrenceProcessor;
23import com.android.calendarcommon.RecurrenceSet;
24import com.android.providers.calendar.CalendarDatabaseHelper.Tables;
25import com.android.providers.calendar.CalendarDatabaseHelper.Views;
26import com.google.common.annotations.VisibleForTesting;
27
28import android.accounts.Account;
29import android.accounts.AccountManager;
30import android.accounts.OnAccountsUpdateListener;
31import android.content.BroadcastReceiver;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.ContentValues;
35import android.content.Context;
36import android.content.Intent;
37import android.content.IntentFilter;
38import android.content.UriMatcher;
39import android.database.Cursor;
40import android.database.DatabaseUtils;
41import android.database.SQLException;
42import android.database.sqlite.SQLiteDatabase;
43import android.database.sqlite.SQLiteQueryBuilder;
44import android.net.Uri;
45import android.os.Handler;
46import android.os.Message;
47import android.os.Process;
48import android.provider.BaseColumns;
49import android.provider.CalendarContract;
50import android.provider.CalendarContract.Attendees;
51import android.provider.CalendarContract.CalendarAlerts;
52import android.provider.CalendarContract.Calendars;
53import android.provider.CalendarContract.Events;
54import android.provider.CalendarContract.Instances;
55import android.provider.CalendarContract.Reminders;
56import android.provider.CalendarContract.SyncState;
57import android.text.TextUtils;
58import android.text.format.DateUtils;
59import android.text.format.Time;
60import android.util.Log;
61import android.util.TimeFormatException;
62import android.util.TimeUtils;
63
64import java.lang.reflect.Array;
65import java.util.ArrayList;
66import java.util.Arrays;
67import java.util.HashMap;
68import java.util.HashSet;
69import java.util.List;
70import java.util.Set;
71import java.util.TimeZone;
72import java.util.regex.Matcher;
73import java.util.regex.Pattern;
74
75/**
76 * Calendar content provider. The contract between this provider and applications
77 * is defined in {@link android.provider.CalendarContract}.
78 */
79public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
80
81
82    protected static final String TAG = "CalendarProvider2";
83    static final boolean DEBUG_INSTANCES = false;
84
85    private static final String TIMEZONE_GMT = "GMT";
86    private static final String ACCOUNT_SELECTION_PREFIX = Calendars.ACCOUNT_NAME + "=? AND "
87            + Calendars.ACCOUNT_TYPE + "=?";
88
89    protected static final boolean PROFILE = false;
90    private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
91
92    private static final String[] ID_ONLY_PROJECTION =
93            new String[] {Events._ID};
94
95    private static final String[] EVENTS_PROJECTION = new String[] {
96            Events._SYNC_ID,
97            Events.RRULE,
98            Events.RDATE,
99            Events.ORIGINAL_ID,
100            Events.ORIGINAL_SYNC_ID,
101    };
102
103    private static final int EVENTS_SYNC_ID_INDEX = 0;
104    private static final int EVENTS_RRULE_INDEX = 1;
105    private static final int EVENTS_RDATE_INDEX = 2;
106    private static final int EVENTS_ORIGINAL_ID_INDEX = 3;
107    private static final int EVENTS_ORIGINAL_SYNC_ID_INDEX = 4;
108
109    private static final String[] ID_PROJECTION = new String[] {
110            Attendees._ID,
111            Attendees.EVENT_ID, // Assume these are the same for each table
112    };
113    private static final int ID_INDEX = 0;
114    private static final int EVENT_ID_INDEX = 1;
115
116    /**
117     * Projection to query for correcting times in allDay events.
118     */
119    private static final String[] ALLDAY_TIME_PROJECTION = new String[] {
120        Events._ID,
121        Events.DTSTART,
122        Events.DTEND,
123        Events.DURATION
124    };
125    private static final int ALLDAY_ID_INDEX = 0;
126    private static final int ALLDAY_DTSTART_INDEX = 1;
127    private static final int ALLDAY_DTEND_INDEX = 2;
128    private static final int ALLDAY_DURATION_INDEX = 3;
129
130    private static final int DAY_IN_SECONDS = 24 * 60 * 60;
131
132    /**
133     * The cached copy of the CalendarMetaData database table.
134     * Make this "package private" instead of "private" so that test code
135     * can access it.
136     */
137    MetaData mMetaData;
138    CalendarCache mCalendarCache;
139
140    private CalendarDatabaseHelper mDbHelper;
141    private CalendarInstancesHelper mInstancesHelper;
142
143    // The extended property name for storing an Event original Timezone.
144    // Due to an issue in Calendar Server restricting the length of the name we
145    // had to strip it down
146    // TODO - Better name would be:
147    // "com.android.providers.calendar.CalendarSyncAdapter#originalTimezone"
148    protected static final String EXT_PROP_ORIGINAL_TIMEZONE =
149        "CalendarSyncAdapter#originalTimezone";
150
151    private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " +
152            CalendarContract.EventsRawTimes.EVENT_ID + ", " +
153            CalendarContract.EventsRawTimes.DTSTART_2445 + ", " +
154            CalendarContract.EventsRawTimes.DTEND_2445 + ", " +
155            Events.EVENT_TIMEZONE +
156            " FROM " +
157            Tables.EVENTS_RAW_TIMES + ", " +
158            Tables.EVENTS +
159            " WHERE " +
160            CalendarContract.EventsRawTimes.EVENT_ID + " = " + Tables.EVENTS + "." + Events._ID;
161
162    private static final String SQL_UPDATE_EVENT_SET_DIRTY = "UPDATE " +
163            Tables.EVENTS +
164            " SET " + Events.DIRTY + "=1" +
165            " WHERE " + Events._ID + "=?";
166
167    protected static final String SQL_WHERE_ID = Events._ID + "=?";
168    private static final String SQL_WHERE_EVENT_ID = "event_id=?";
169    private static final String SQL_WHERE_ORIGINAL_ID = Events.ORIGINAL_ID + "=?";
170    private static final String SQL_WHERE_ORIGINAL_ID_NO_SYNC_ID = Events.ORIGINAL_ID +
171            "=? AND " + Events._SYNC_ID + " IS NULL";
172
173    private static final String SQL_WHERE_ATTENDEE_BASE =
174            Tables.EVENTS + "." + Events._ID + "=" + Tables.ATTENDEES + "." + Attendees.EVENT_ID
175            + " AND " +
176            Tables.EVENTS + "." + Events.CALENDAR_ID + "=" + Tables.CALENDARS + "." + Calendars._ID;
177
178    private static final String SQL_WHERE_ATTENDEES_ID =
179            Tables.ATTENDEES + "." + Attendees._ID + "=? AND " + SQL_WHERE_ATTENDEE_BASE;
180
181    private static final String SQL_WHERE_REMINDERS_ID =
182            Tables.REMINDERS + "." + Reminders._ID + "=? AND " +
183            Tables.EVENTS + "." + Events._ID + "=" + Tables.REMINDERS + "." + Reminders.EVENT_ID +
184            " AND " +
185            Tables.EVENTS + "." + Events.CALENDAR_ID + "=" + Tables.CALENDARS + "." + Calendars._ID;
186
187    private static final String SQL_WHERE_CALENDAR_ALERT =
188            Views.EVENTS + "." + Events._ID + "=" +
189                    Tables.CALENDAR_ALERTS + "." + CalendarAlerts.EVENT_ID;
190
191    private static final String SQL_WHERE_CALENDAR_ALERT_ID =
192            Views.EVENTS + "." + Events._ID + "=" +
193                    Tables.CALENDAR_ALERTS + "." + CalendarAlerts.EVENT_ID +
194            " AND " +
195            Tables.CALENDAR_ALERTS + "." + CalendarAlerts._ID + "=?";
196
197    private static final String SQL_WHERE_EXTENDED_PROPERTIES_ID =
198            Tables.EXTENDED_PROPERTIES + "." + CalendarContract.ExtendedProperties._ID + "=?";
199
200    private static final String SQL_DELETE_FROM_CALENDARS = "DELETE FROM " + Tables.CALENDARS +
201                " WHERE " + Calendars.ACCOUNT_NAME + "=? AND " +
202                    Calendars.ACCOUNT_TYPE + "=?";
203
204    private static final String SQL_SELECT_COUNT_FOR_SYNC_ID =
205            "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?";
206
207    // Make sure we load at least two months worth of data.
208    // Client apps can load more data in a background thread.
209    private static final long MINIMUM_EXPANSION_SPAN =
210            2L * 31 * 24 * 60 * 60 * 1000;
211
212    private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
213    private static final int CALENDARS_INDEX_ID = 0;
214
215    private static final String INSTANCE_QUERY_TABLES =
216        CalendarDatabaseHelper.Tables.INSTANCES + " INNER JOIN " +
217        CalendarDatabaseHelper.Views.EVENTS + " AS " +
218        CalendarDatabaseHelper.Tables.EVENTS +
219        " ON (" + CalendarDatabaseHelper.Tables.INSTANCES + "."
220        + CalendarContract.Instances.EVENT_ID + "=" +
221        CalendarDatabaseHelper.Tables.EVENTS + "."
222        + CalendarContract.Events._ID + ")";
223
224    private static final String INSTANCE_SEARCH_QUERY_TABLES = "(" +
225        CalendarDatabaseHelper.Tables.INSTANCES + " INNER JOIN " +
226        CalendarDatabaseHelper.Views.EVENTS + " AS " +
227        CalendarDatabaseHelper.Tables.EVENTS +
228        " ON (" + CalendarDatabaseHelper.Tables.INSTANCES + "."
229        + CalendarContract.Instances.EVENT_ID + "=" +
230        CalendarDatabaseHelper.Tables.EVENTS + "."
231        + CalendarContract.Events._ID + ")" + ") LEFT OUTER JOIN " +
232        CalendarDatabaseHelper.Tables.ATTENDEES +
233        " ON (" + CalendarDatabaseHelper.Tables.ATTENDEES + "."
234        + CalendarContract.Attendees.EVENT_ID + "=" +
235        CalendarDatabaseHelper.Tables.EVENTS + "."
236        + CalendarContract.Events._ID + ")";
237
238    private static final String SQL_WHERE_INSTANCES_BETWEEN_DAY =
239        CalendarContract.Instances.START_DAY + "<=? AND " +
240        CalendarContract.Instances.END_DAY + ">=?";
241
242    private static final String SQL_WHERE_INSTANCES_BETWEEN =
243        CalendarContract.Instances.BEGIN + "<=? AND " +
244        CalendarContract.Instances.END + ">=?";
245
246    private static final int INSTANCES_INDEX_START_DAY = 0;
247    private static final int INSTANCES_INDEX_END_DAY = 1;
248    private static final int INSTANCES_INDEX_START_MINUTE = 2;
249    private static final int INSTANCES_INDEX_END_MINUTE = 3;
250    private static final int INSTANCES_INDEX_ALL_DAY = 4;
251
252    /**
253     * The sort order is: events with an earlier start time occur first and if
254     * the start times are the same, then events with a later end time occur
255     * first. The later end time is ordered first so that long-running events in
256     * the calendar views appear first. If the start and end times of two events
257     * are the same then we sort alphabetically on the title. This isn't
258     * required for correctness, it just adds a nice touch.
259     */
260    public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC";
261
262    /**
263     * A regex for describing how we split search queries into tokens. Keeps
264     * quoted phrases as one token. "one \"two three\"" ==> ["one" "two three"]
265     */
266    private static final Pattern SEARCH_TOKEN_PATTERN =
267        Pattern.compile("[^\\s\"'.?!,]+|" // first part matches unquoted words
268                      + "\"([^\"]*)\"");  // second part matches quoted phrases
269    /**
270     * A special character that was use to escape potentially problematic
271     * characters in search queries.
272     *
273     * Note: do not use backslash for this, as it interferes with the regex
274     * escaping mechanism.
275     */
276    private static final String SEARCH_ESCAPE_CHAR = "#";
277
278    /**
279     * A regex for matching any characters in an incoming search query that we
280     * need to escape with {@link #SEARCH_ESCAPE_CHAR}, including the escape
281     * character itself.
282     */
283    private static final Pattern SEARCH_ESCAPE_PATTERN =
284        Pattern.compile("([%_" + SEARCH_ESCAPE_CHAR + "])");
285
286    /**
287     * Alias used for aggregate concatenation of attendee e-mails when grouping
288     * attendees by instance.
289     */
290    private static final String ATTENDEES_EMAIL_CONCAT =
291        "group_concat(" + CalendarContract.Attendees.ATTENDEE_EMAIL + ")";
292
293    /**
294     * Alias used for aggregate concatenation of attendee names when grouping
295     * attendees by instance.
296     */
297    private static final String ATTENDEES_NAME_CONCAT =
298        "group_concat(" + CalendarContract.Attendees.ATTENDEE_NAME + ")";
299
300    private static final String[] SEARCH_COLUMNS = new String[] {
301        CalendarContract.Events.TITLE,
302        CalendarContract.Events.DESCRIPTION,
303        CalendarContract.Events.EVENT_LOCATION,
304        ATTENDEES_EMAIL_CONCAT,
305        ATTENDEES_NAME_CONCAT
306    };
307
308    /**
309     * Arbitrary integer that we assign to the messages that we send to this
310     * thread's handler, indicating that these are requests to send an update
311     * notification intent.
312     */
313    private static final int UPDATE_BROADCAST_MSG = 1;
314
315    /**
316     * Any requests to send a PROVIDER_CHANGED intent will be collapsed over
317     * this window, to prevent spamming too many intents at once.
318     */
319    private static final long UPDATE_BROADCAST_TIMEOUT_MILLIS =
320        DateUtils.SECOND_IN_MILLIS;
321
322    private static final long SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS =
323        30 * DateUtils.SECOND_IN_MILLIS;
324
325    /** Set of columns allowed to be altered when creating an exception to a recurring event. */
326    private static final HashSet<String> ALLOWED_IN_EXCEPTION = new HashSet<String>();
327    static {
328        // _id, _sync_account, _sync_account_type, dirty, _sync_mark, calendar_id
329        ALLOWED_IN_EXCEPTION.add(Events._SYNC_ID);
330        ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA1);
331        ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA7);
332        ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA3);
333        ALLOWED_IN_EXCEPTION.add(Events.TITLE);
334        ALLOWED_IN_EXCEPTION.add(Events.EVENT_LOCATION);
335        ALLOWED_IN_EXCEPTION.add(Events.DESCRIPTION);
336        ALLOWED_IN_EXCEPTION.add(Events.STATUS);
337        ALLOWED_IN_EXCEPTION.add(Events.SELF_ATTENDEE_STATUS);
338        ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA6);
339        ALLOWED_IN_EXCEPTION.add(Events.DTSTART);
340        // dtend -- set from duration as part of creating the exception
341        ALLOWED_IN_EXCEPTION.add(Events.EVENT_TIMEZONE);
342        ALLOWED_IN_EXCEPTION.add(Events.EVENT_END_TIMEZONE);
343        ALLOWED_IN_EXCEPTION.add(Events.DURATION);
344        ALLOWED_IN_EXCEPTION.add(Events.ALL_DAY);
345        ALLOWED_IN_EXCEPTION.add(Events.ACCESS_LEVEL);
346        ALLOWED_IN_EXCEPTION.add(Events.AVAILABILITY);
347        ALLOWED_IN_EXCEPTION.add(Events.HAS_ALARM);
348        ALLOWED_IN_EXCEPTION.add(Events.HAS_EXTENDED_PROPERTIES);
349        ALLOWED_IN_EXCEPTION.add(Events.RRULE);
350        ALLOWED_IN_EXCEPTION.add(Events.RDATE);
351        ALLOWED_IN_EXCEPTION.add(Events.EXRULE);
352        ALLOWED_IN_EXCEPTION.add(Events.EXDATE);
353        ALLOWED_IN_EXCEPTION.add(Events.ORIGINAL_SYNC_ID);
354        ALLOWED_IN_EXCEPTION.add(Events.ORIGINAL_INSTANCE_TIME);
355        // originalAllDay, lastDate
356        ALLOWED_IN_EXCEPTION.add(Events.HAS_ATTENDEE_DATA);
357        ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_MODIFY);
358        ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_INVITE_OTHERS);
359        ALLOWED_IN_EXCEPTION.add(Events.GUESTS_CAN_SEE_GUESTS);
360        ALLOWED_IN_EXCEPTION.add(Events.ORGANIZER);
361        // deleted, original_id, alerts
362    }
363
364    /** Don't clone these from the base event into the exception event. */
365    private static final String[] DONT_CLONE_INTO_EXCEPTION = {
366        Events._SYNC_ID,
367        Events.SYNC_DATA1,
368        Events.SYNC_DATA2,
369        Events.SYNC_DATA3,
370        Events.SYNC_DATA4,
371        Events.SYNC_DATA5,
372        Events.SYNC_DATA6,
373        Events.SYNC_DATA7,
374        Events.SYNC_DATA8,
375        Events.SYNC_DATA9,
376        Events.SYNC_DATA10,
377    };
378
379    /** set to 'true' to enable debug logging for recurrence exception code */
380    private static final boolean DEBUG_EXCEPTION = false;
381
382    private Context mContext;
383    private ContentResolver mContentResolver;
384
385    private static CalendarProvider2 mInstance;
386
387    @VisibleForTesting
388    protected CalendarAlarmManager mCalendarAlarm;
389
390    private final Handler mBroadcastHandler = new Handler() {
391        @Override
392        public void handleMessage(Message msg) {
393            Context context = CalendarProvider2.this.mContext;
394            if (msg.what == UPDATE_BROADCAST_MSG) {
395                // Broadcast a provider changed intent
396                doSendUpdateNotification();
397                // Because the handler does not guarantee message delivery in
398                // the case that the provider is killed, we need to make sure
399                // that the provider stays alive long enough to deliver the
400                // notification. This empty service is sufficient to "wedge" the
401                // process until we stop it here.
402                context.stopService(new Intent(context, EmptyService.class));
403            }
404        }
405    };
406
407    /**
408     * Listens for timezone changes and disk-no-longer-full events
409     */
410    private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
411        @Override
412        public void onReceive(Context context, Intent intent) {
413            String action = intent.getAction();
414            if (Log.isLoggable(TAG, Log.DEBUG)) {
415                Log.d(TAG, "onReceive() " + action);
416            }
417            if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
418                updateTimezoneDependentFields();
419                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
420            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
421                // Try to clean up if things were screwy due to a full disk
422                updateTimezoneDependentFields();
423                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
424            } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
425                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
426            }
427        }
428    };
429
430    protected void verifyAccounts() {
431        AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
432        onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
433    }
434
435    /* Visible for testing */
436    @Override
437    protected CalendarDatabaseHelper getDatabaseHelper(final Context context) {
438        return CalendarDatabaseHelper.getInstance(context);
439    }
440
441    protected static CalendarProvider2 getInstance() {
442        return mInstance;
443    }
444
445    @Override
446    public void shutdown() {
447        if (mDbHelper != null) {
448            mDbHelper.close();
449            mDbHelper = null;
450            mDb = null;
451        }
452    }
453
454    @Override
455    public boolean onCreate() {
456        super.onCreate();
457        try {
458            return initialize();
459        } catch (RuntimeException e) {
460            if (Log.isLoggable(TAG, Log.ERROR)) {
461                Log.e(TAG, "Cannot start provider", e);
462            }
463            return false;
464        }
465    }
466
467    private boolean initialize() {
468        mInstance = this;
469
470        mContext = getContext();
471        mContentResolver = mContext.getContentResolver();
472
473        mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper();
474        mDb = mDbHelper.getWritableDatabase();
475
476        mMetaData = new MetaData(mDbHelper);
477        mInstancesHelper = new CalendarInstancesHelper(mDbHelper, mMetaData);
478
479        // Register for Intent broadcasts
480        IntentFilter filter = new IntentFilter();
481
482        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
483        filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
484        filter.addAction(Intent.ACTION_TIME_CHANGED);
485
486        // We don't ever unregister this because this thread always wants
487        // to receive notifications, even in the background.  And if this
488        // thread is killed then the whole process will be killed and the
489        // memory resources will be reclaimed.
490        mContext.registerReceiver(mIntentReceiver, filter);
491
492        mCalendarCache = new CalendarCache(mDbHelper);
493
494        // This is pulled out for testing
495        initCalendarAlarm();
496
497        postInitialize();
498
499        return true;
500    }
501
502    protected void initCalendarAlarm() {
503        mCalendarAlarm = getOrCreateCalendarAlarmManager();
504        mCalendarAlarm.getScheduleNextAlarmWakeLock();
505    }
506
507    synchronized CalendarAlarmManager getOrCreateCalendarAlarmManager() {
508        if (mCalendarAlarm == null) {
509            mCalendarAlarm = new CalendarAlarmManager(mContext);
510        }
511        return mCalendarAlarm;
512    }
513
514    protected void postInitialize() {
515        Thread thread = new PostInitializeThread();
516        thread.start();
517    }
518
519    private class PostInitializeThread extends Thread {
520        @Override
521        public void run() {
522            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
523
524            verifyAccounts();
525
526            doUpdateTimezoneDependentFields();
527        }
528    }
529
530    /**
531     * This creates a background thread to check the timezone and update
532     * the timezone dependent fields in the Instances table if the timezone
533     * has changed.
534     */
535    protected void updateTimezoneDependentFields() {
536        Thread thread = new TimezoneCheckerThread();
537        thread.start();
538    }
539
540    private class TimezoneCheckerThread extends Thread {
541        @Override
542        public void run() {
543            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
544            doUpdateTimezoneDependentFields();
545        }
546    }
547
548    /**
549     * Check if we are in the same time zone
550     */
551    private boolean isLocalSameAsInstancesTimezone() {
552        String localTimezone = TimeZone.getDefault().getID();
553        return TextUtils.equals(mCalendarCache.readTimezoneInstances(), localTimezone);
554    }
555
556    /**
557     * This method runs in a background thread.  If the timezone has changed
558     * then the Instances table will be regenerated.
559     */
560    protected void doUpdateTimezoneDependentFields() {
561        try {
562            String timezoneType = mCalendarCache.readTimezoneType();
563            // Nothing to do if we have the "home" timezone type (timezone is sticky)
564            if (timezoneType != null && timezoneType.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
565                return;
566            }
567            // We are here in "auto" mode, the timezone is coming from the device
568            if (! isSameTimezoneDatabaseVersion()) {
569                String localTimezone = TimeZone.getDefault().getID();
570                doProcessEventRawTimes(localTimezone, TimeUtils.getTimeZoneDatabaseVersion());
571            }
572            if (isLocalSameAsInstancesTimezone()) {
573                // Even if the timezone hasn't changed, check for missed alarms.
574                // This code executes when the CalendarProvider2 is created and
575                // helps to catch missed alarms when the Calendar process is
576                // killed (because of low-memory conditions) and then restarted.
577                mCalendarAlarm.rescheduleMissedAlarms();
578            }
579        } catch (SQLException e) {
580            if (Log.isLoggable(TAG, Log.ERROR)) {
581                Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
582            }
583            try {
584                // Clear at least the in-memory data (and if possible the
585                // database fields) to force a re-computation of Instances.
586                mMetaData.clearInstanceRange();
587            } catch (SQLException e2) {
588                if (Log.isLoggable(TAG, Log.ERROR)) {
589                    Log.e(TAG, "clearInstanceRange() also failed: " + e2);
590                }
591            }
592        }
593    }
594
595    protected void doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion) {
596        mDb.beginTransaction();
597        try {
598            updateEventsStartEndFromEventRawTimesLocked();
599            updateTimezoneDatabaseVersion(timeZoneDatabaseVersion);
600            mCalendarCache.writeTimezoneInstances(localTimezone);
601            regenerateInstancesTable();
602            mDb.setTransactionSuccessful();
603        } finally {
604            mDb.endTransaction();
605        }
606    }
607
608    private void updateEventsStartEndFromEventRawTimesLocked() {
609        Cursor cursor = mDb.rawQuery(SQL_SELECT_EVENTSRAWTIMES, null /* selection args */);
610        try {
611            while (cursor.moveToNext()) {
612                long eventId = cursor.getLong(0);
613                String dtStart2445 = cursor.getString(1);
614                String dtEnd2445 = cursor.getString(2);
615                String eventTimezone = cursor.getString(3);
616                if (dtStart2445 == null && dtEnd2445 == null) {
617                    if (Log.isLoggable(TAG, Log.ERROR)) {
618                        Log.e(TAG, "Event " + eventId + " has dtStart2445 and dtEnd2445 null "
619                                + "at the same time in EventsRawTimes!");
620                    }
621                    continue;
622                }
623                updateEventsStartEndLocked(eventId,
624                        eventTimezone,
625                        dtStart2445,
626                        dtEnd2445);
627            }
628        } finally {
629            cursor.close();
630            cursor = null;
631        }
632    }
633
634    private long get2445ToMillis(String timezone, String dt2445) {
635        if (null == dt2445) {
636            if (Log.isLoggable(TAG, Log.VERBOSE)) {
637                Log.v(TAG, "Cannot parse null RFC2445 date");
638            }
639            return 0;
640        }
641        Time time = (timezone != null) ? new Time(timezone) : new Time();
642        try {
643            time.parse(dt2445);
644        } catch (TimeFormatException e) {
645            if (Log.isLoggable(TAG, Log.ERROR)) {
646                Log.e(TAG, "Cannot parse RFC2445 date " + dt2445);
647            }
648            return 0;
649        }
650        return time.toMillis(true /* ignore DST */);
651    }
652
653    private void updateEventsStartEndLocked(long eventId,
654            String timezone, String dtStart2445, String dtEnd2445) {
655
656        ContentValues values = new ContentValues();
657        values.put(Events.DTSTART, get2445ToMillis(timezone, dtStart2445));
658        values.put(Events.DTEND, get2445ToMillis(timezone, dtEnd2445));
659
660        int result = mDb.update(Tables.EVENTS, values, SQL_WHERE_ID,
661                new String[] {String.valueOf(eventId)});
662        if (0 == result) {
663            if (Log.isLoggable(TAG, Log.VERBOSE)) {
664                Log.v(TAG, "Could not update Events table with values " + values);
665            }
666        }
667    }
668
669    private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) {
670        try {
671            mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion);
672        } catch (CalendarCache.CacheException e) {
673            if (Log.isLoggable(TAG, Log.ERROR)) {
674                Log.e(TAG, "Could not write timezone database version in the cache");
675            }
676        }
677    }
678
679    /**
680     * Check if the time zone database version is the same as the cached one
681     */
682    protected boolean isSameTimezoneDatabaseVersion() {
683        String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
684        if (timezoneDatabaseVersion == null) {
685            return false;
686        }
687        return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion());
688    }
689
690    @VisibleForTesting
691    protected String getTimezoneDatabaseVersion() {
692        String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
693        if (timezoneDatabaseVersion == null) {
694            return "";
695        }
696        if (Log.isLoggable(TAG, Log.INFO)) {
697            Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion);
698        }
699        return timezoneDatabaseVersion;
700    }
701
702    private boolean isHomeTimezone() {
703        String type = mCalendarCache.readTimezoneType();
704        return type.equals(CalendarCache.TIMEZONE_TYPE_HOME);
705    }
706
707    private void regenerateInstancesTable() {
708        // The database timezone is different from the current timezone.
709        // Regenerate the Instances table for this month.  Include events
710        // starting at the beginning of this month.
711        long now = System.currentTimeMillis();
712        String instancesTimezone = mCalendarCache.readTimezoneInstances();
713        Time time = new Time(instancesTimezone);
714        time.set(now);
715        time.monthDay = 1;
716        time.hour = 0;
717        time.minute = 0;
718        time.second = 0;
719
720        long begin = time.normalize(true);
721        long end = begin + MINIMUM_EXPANSION_SPAN;
722
723        Cursor cursor = null;
724        try {
725            cursor = handleInstanceQuery(new SQLiteQueryBuilder(),
726                    begin, end,
727                    new String[] { Instances._ID },
728                    null /* selection */, null,
729                    null /* sort */,
730                    false /* searchByDayInsteadOfMillis */,
731                    true /* force Instances deletion and expansion */,
732                    instancesTimezone, isHomeTimezone());
733        } finally {
734            if (cursor != null) {
735                cursor.close();
736            }
737        }
738
739        mCalendarAlarm.rescheduleMissedAlarms();
740    }
741
742
743    @Override
744    protected void notifyChange(boolean syncToNetwork) {
745        // Note that semantics are changed: notification is for CONTENT_URI, not the specific
746        // Uri that was modified.
747        mContentResolver.notifyChange(CalendarContract.CONTENT_URI, null, syncToNetwork);
748    }
749
750    @Override
751    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
752            String sortOrder) {
753        if (Log.isLoggable(TAG, Log.VERBOSE)) {
754            Log.v(TAG, "query uri - " + uri);
755        }
756
757        final SQLiteDatabase db = mDbHelper.getReadableDatabase();
758
759        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
760        String groupBy = null;
761        String limit = null; // Not currently implemented
762        String instancesTimezone;
763
764        final int match = sUriMatcher.match(uri);
765        switch (match) {
766            case SYNCSTATE:
767                return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
768                        sortOrder);
769
770            case EVENTS:
771                qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
772                qb.setProjectionMap(sEventsProjectionMap);
773                selection = appendAccountFromParameterToSelection(selection, uri);
774                selection = appendLastSyncedColumnToSelection(selection, uri);
775                break;
776            case EVENTS_ID:
777                qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
778                qb.setProjectionMap(sEventsProjectionMap);
779                selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
780                qb.appendWhere(SQL_WHERE_ID);
781                break;
782
783            case EVENT_ENTITIES:
784                qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
785                qb.setProjectionMap(sEventEntitiesProjectionMap);
786                selection = appendAccountFromParameterToSelection(selection, uri);
787                selection = appendLastSyncedColumnToSelection(selection, uri);
788                break;
789            case EVENT_ENTITIES_ID:
790                qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
791                qb.setProjectionMap(sEventEntitiesProjectionMap);
792                selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
793                qb.appendWhere(SQL_WHERE_ID);
794                break;
795
796            case CALENDARS:
797            case CALENDAR_ENTITIES:
798                qb.setTables(Tables.CALENDARS);
799                selection = appendAccountFromParameterToSelection(selection, uri);
800                break;
801            case CALENDARS_ID:
802            case CALENDAR_ENTITIES_ID:
803                qb.setTables(Tables.CALENDARS);
804                selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
805                qb.appendWhere(SQL_WHERE_ID);
806                break;
807            case INSTANCES:
808            case INSTANCES_BY_DAY:
809                long begin;
810                long end;
811                try {
812                    begin = Long.valueOf(uri.getPathSegments().get(2));
813                } catch (NumberFormatException nfe) {
814                    throw new IllegalArgumentException("Cannot parse begin "
815                            + uri.getPathSegments().get(2));
816                }
817                try {
818                    end = Long.valueOf(uri.getPathSegments().get(3));
819                } catch (NumberFormatException nfe) {
820                    throw new IllegalArgumentException("Cannot parse end "
821                            + uri.getPathSegments().get(3));
822                }
823                instancesTimezone = mCalendarCache.readTimezoneInstances();
824                return handleInstanceQuery(qb, begin, end, projection, selection, selectionArgs,
825                        sortOrder, match == INSTANCES_BY_DAY, false /* don't force an expansion */,
826                        instancesTimezone, isHomeTimezone());
827            case INSTANCES_SEARCH:
828            case INSTANCES_SEARCH_BY_DAY:
829                try {
830                    begin = Long.valueOf(uri.getPathSegments().get(2));
831                } catch (NumberFormatException nfe) {
832                    throw new IllegalArgumentException("Cannot parse begin "
833                            + uri.getPathSegments().get(2));
834                }
835                try {
836                    end = Long.valueOf(uri.getPathSegments().get(3));
837                } catch (NumberFormatException nfe) {
838                    throw new IllegalArgumentException("Cannot parse end "
839                            + uri.getPathSegments().get(3));
840                }
841                instancesTimezone = mCalendarCache.readTimezoneInstances();
842                // this is already decoded
843                String query = uri.getPathSegments().get(4);
844                return handleInstanceSearchQuery(qb, begin, end, query, projection, selection,
845                        selectionArgs, sortOrder, match == INSTANCES_SEARCH_BY_DAY,
846                        instancesTimezone, isHomeTimezone());
847            case EVENT_DAYS:
848                int startDay;
849                int endDay;
850                try {
851                    startDay = Integer.valueOf(uri.getPathSegments().get(2));
852                } catch (NumberFormatException nfe) {
853                    throw new IllegalArgumentException("Cannot parse start day "
854                            + uri.getPathSegments().get(2));
855                }
856                try {
857                    endDay = Integer.valueOf(uri.getPathSegments().get(3));
858                } catch (NumberFormatException nfe) {
859                    throw new IllegalArgumentException("Cannot parse end day "
860                            + uri.getPathSegments().get(3));
861                }
862                instancesTimezone = mCalendarCache.readTimezoneInstances();
863                return handleEventDayQuery(qb, startDay, endDay, projection, selection,
864                        instancesTimezone, isHomeTimezone());
865            case ATTENDEES:
866                qb.setTables(Tables.ATTENDEES + ", " + Tables.EVENTS + ", " + Tables.CALENDARS);
867                qb.setProjectionMap(sAttendeesProjectionMap);
868                qb.appendWhere(SQL_WHERE_ATTENDEE_BASE);
869                break;
870            case ATTENDEES_ID:
871                qb.setTables(Tables.ATTENDEES + ", " + Tables.EVENTS + ", " + Tables.CALENDARS);
872                qb.setProjectionMap(sAttendeesProjectionMap);
873                selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
874                qb.appendWhere(SQL_WHERE_ATTENDEES_ID);
875                break;
876            case REMINDERS:
877                qb.setTables(Tables.REMINDERS);
878                break;
879            case REMINDERS_ID:
880                qb.setTables(Tables.REMINDERS + ", " + Tables.EVENTS + ", " + Tables.CALENDARS);
881                qb.setProjectionMap(sRemindersProjectionMap);
882                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
883                qb.appendWhere(SQL_WHERE_REMINDERS_ID);
884                break;
885            case CALENDAR_ALERTS:
886                qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS);
887                qb.setProjectionMap(sCalendarAlertsProjectionMap);
888                qb.appendWhere(SQL_WHERE_CALENDAR_ALERT);
889                break;
890            case CALENDAR_ALERTS_BY_INSTANCE:
891                qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS);
892                qb.setProjectionMap(sCalendarAlertsProjectionMap);
893                qb.appendWhere(SQL_WHERE_CALENDAR_ALERT);
894                groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
895                break;
896            case CALENDAR_ALERTS_ID:
897                qb.setTables(Tables.CALENDAR_ALERTS + ", " + CalendarDatabaseHelper.Views.EVENTS);
898                qb.setProjectionMap(sCalendarAlertsProjectionMap);
899                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
900                qb.appendWhere(SQL_WHERE_CALENDAR_ALERT_ID);
901                break;
902            case EXTENDED_PROPERTIES:
903                qb.setTables(Tables.EXTENDED_PROPERTIES);
904                break;
905            case EXTENDED_PROPERTIES_ID:
906                qb.setTables(Tables.EXTENDED_PROPERTIES);
907                selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
908                qb.appendWhere(SQL_WHERE_EXTENDED_PROPERTIES_ID);
909                break;
910            case PROVIDER_PROPERTIES:
911                qb.setTables(Tables.CALENDAR_CACHE);
912                qb.setProjectionMap(sCalendarCacheProjectionMap);
913                break;
914            default:
915                throw new IllegalArgumentException("Unknown URL " + uri);
916        }
917
918        // run the query
919        return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
920    }
921
922    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
923            String selection, String[] selectionArgs, String sortOrder, String groupBy,
924            String limit) {
925
926        if (projection != null && projection.length == 1
927                && BaseColumns._COUNT.equals(projection[0])) {
928            qb.setProjectionMap(sCountProjectionMap);
929        }
930
931        if (Log.isLoggable(TAG, Log.VERBOSE)) {
932            Log.v(TAG, "query sql - projection: " + Arrays.toString(projection) +
933                    " selection: " + selection +
934                    " selectionArgs: " + Arrays.toString(selectionArgs) +
935                    " sortOrder: " + sortOrder +
936                    " groupBy: " + groupBy +
937                    " limit: " + limit);
938        }
939        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
940                sortOrder, limit);
941        if (c != null) {
942            // TODO: is this the right notification Uri?
943            c.setNotificationUri(mContentResolver, CalendarContract.Events.CONTENT_URI);
944        }
945        return c;
946    }
947
948    /*
949     * Fills the Instances table, if necessary, for the given range and then
950     * queries the Instances table.
951     *
952     * @param qb The query
953     * @param rangeBegin start of range (Julian days or ms)
954     * @param rangeEnd end of range (Julian days or ms)
955     * @param projection The projection
956     * @param selection The selection
957     * @param sort How to sort
958     * @param searchByDay if true, range is in Julian days, if false, range is in ms
959     * @param forceExpansion force the Instance deletion and expansion if set to true
960     * @param instancesTimezone timezone we need to use for computing the instances
961     * @param isHomeTimezone if true, we are in the "home" timezone
962     * @return
963     */
964    private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
965            long rangeEnd, String[] projection, String selection, String[] selectionArgs,
966            String sort, boolean searchByDay, boolean forceExpansion,
967            String instancesTimezone, boolean isHomeTimezone) {
968
969        qb.setTables(INSTANCE_QUERY_TABLES);
970        qb.setProjectionMap(sInstancesProjectionMap);
971        if (searchByDay) {
972            // Convert the first and last Julian day range to a range that uses
973            // UTC milliseconds.
974            Time time = new Time(instancesTimezone);
975            long beginMs = time.setJulianDay((int) rangeBegin);
976            // We add one to lastDay because the time is set to 12am on the given
977            // Julian day and we want to include all the events on the last day.
978            long endMs = time.setJulianDay((int) rangeEnd + 1);
979            // will lock the database.
980            acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */,
981                    forceExpansion, instancesTimezone, isHomeTimezone);
982            qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY);
983        } else {
984            // will lock the database.
985            acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */,
986                    forceExpansion, instancesTimezone, isHomeTimezone);
987            qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN);
988        }
989
990        String[] newSelectionArgs = new String[] {String.valueOf(rangeEnd),
991                String.valueOf(rangeBegin)};
992        if (selectionArgs == null) {
993            selectionArgs = newSelectionArgs;
994        } else {
995            // The appendWhere pieces get added first, so put the
996            // newSelectionArgs first.
997            selectionArgs = combine(newSelectionArgs, selectionArgs);
998        }
999        return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */,
1000                null /* having */, sort);
1001    }
1002
1003    /**
1004     * Combine a set of arrays in the order they are passed in. All arrays must
1005     * be of the same type.
1006     */
1007    private static <T> T[] combine(T[]... arrays) {
1008        if (arrays.length == 0) {
1009            throw new IllegalArgumentException("Must supply at least 1 array to combine");
1010        }
1011
1012        int totalSize = 0;
1013        for (T[] array : arrays) {
1014            totalSize += array.length;
1015        }
1016
1017        T[] finalArray = (T[]) (Array.newInstance(arrays[0].getClass().getComponentType(),
1018                totalSize));
1019
1020        int currentPos = 0;
1021        for (T[] array : arrays) {
1022            int length = array.length;
1023            System.arraycopy(array, 0, finalArray, currentPos, length);
1024            currentPos += array.length;
1025        }
1026        return finalArray;
1027    }
1028
1029    /**
1030     * Escape any special characters in the search token
1031     * @param token the token to escape
1032     * @return the escaped token
1033     */
1034    @VisibleForTesting
1035    String escapeSearchToken(String token) {
1036        Matcher matcher = SEARCH_ESCAPE_PATTERN.matcher(token);
1037        return matcher.replaceAll(SEARCH_ESCAPE_CHAR + "$1");
1038    }
1039
1040    /**
1041     * Splits the search query into individual search tokens based on whitespace
1042     * and punctuation. Leaves both single quoted and double quoted strings
1043     * intact.
1044     *
1045     * @param query the search query
1046     * @return an array of tokens from the search query
1047     */
1048    @VisibleForTesting
1049    String[] tokenizeSearchQuery(String query) {
1050        List<String> matchList = new ArrayList<String>();
1051        Matcher matcher = SEARCH_TOKEN_PATTERN.matcher(query);
1052        String token;
1053        while (matcher.find()) {
1054            if (matcher.group(1) != null) {
1055                // double quoted string
1056                token = matcher.group(1);
1057            } else {
1058                // unquoted token
1059                token = matcher.group();
1060            }
1061            matchList.add(escapeSearchToken(token));
1062        }
1063        return matchList.toArray(new String[matchList.size()]);
1064    }
1065
1066    /**
1067     * In order to support what most people would consider a reasonable
1068     * search behavior, we have to do some interesting things here. We
1069     * assume that when a user searches for something like "lunch meeting",
1070     * they really want any event that matches both "lunch" and "meeting",
1071     * not events that match the string "lunch meeting" itself. In order to
1072     * do this across multiple columns, we have to construct a WHERE clause
1073     * that looks like:
1074     * <code>
1075     *   WHERE (title LIKE "%lunch%"
1076     *      OR description LIKE "%lunch%"
1077     *      OR eventLocation LIKE "%lunch%")
1078     *     AND (title LIKE "%meeting%"
1079     *      OR description LIKE "%meeting%"
1080     *      OR eventLocation LIKE "%meeting%")
1081     * </code>
1082     * This "product of clauses" is a bit ugly, but produced a fairly good
1083     * approximation of full-text search across multiple columns.
1084     */
1085    @VisibleForTesting
1086    String constructSearchWhere(String[] tokens) {
1087        if (tokens.length == 0) {
1088            return "";
1089        }
1090        StringBuilder sb = new StringBuilder();
1091        String column, token;
1092        for (int j = 0; j < tokens.length; j++) {
1093            sb.append("(");
1094            for (int i = 0; i < SEARCH_COLUMNS.length; i++) {
1095                sb.append(SEARCH_COLUMNS[i]);
1096                sb.append(" LIKE ? ESCAPE \"");
1097                sb.append(SEARCH_ESCAPE_CHAR);
1098                sb.append("\" ");
1099                if (i < SEARCH_COLUMNS.length - 1) {
1100                    sb.append("OR ");
1101                }
1102            }
1103            sb.append(")");
1104            if (j < tokens.length - 1) {
1105                sb.append(" AND ");
1106            }
1107        }
1108        return sb.toString();
1109    }
1110
1111    @VisibleForTesting
1112    String[] constructSearchArgs(String[] tokens, long rangeBegin, long rangeEnd) {
1113        int numCols = SEARCH_COLUMNS.length;
1114        int numArgs = tokens.length * numCols + 2;
1115        // the additional two elements here are for begin/end time
1116        String[] selectionArgs = new String[numArgs];
1117        selectionArgs[0] =  String.valueOf(rangeEnd);
1118        selectionArgs[1] =  String.valueOf(rangeBegin);
1119        for (int j = 0; j < tokens.length; j++) {
1120            int start = 2 + numCols * j;
1121            for (int i = start; i < start + numCols; i++) {
1122                selectionArgs[i] = "%" + tokens[j] + "%";
1123            }
1124        }
1125        return selectionArgs;
1126    }
1127
1128    private Cursor handleInstanceSearchQuery(SQLiteQueryBuilder qb,
1129            long rangeBegin, long rangeEnd, String query, String[] projection,
1130            String selection, String[] selectionArgs, String sort, boolean searchByDay,
1131            String instancesTimezone, boolean isHomeTimezone) {
1132        qb.setTables(INSTANCE_SEARCH_QUERY_TABLES);
1133        qb.setProjectionMap(sInstancesProjectionMap);
1134
1135        String[] tokens = tokenizeSearchQuery(query);
1136        String[] newSelectionArgs = constructSearchArgs(tokens, rangeBegin, rangeEnd);
1137        if (selectionArgs == null) {
1138            selectionArgs = newSelectionArgs;
1139        } else {
1140            // The appendWhere pieces get added first, so put the
1141            // newSelectionArgs first.
1142            selectionArgs = combine(newSelectionArgs, selectionArgs);
1143        }
1144        // we pass this in as a HAVING instead of a WHERE so the filtering
1145        // happens after the grouping
1146        String searchWhere = constructSearchWhere(tokens);
1147
1148        if (searchByDay) {
1149            // Convert the first and last Julian day range to a range that uses
1150            // UTC milliseconds.
1151            Time time = new Time(instancesTimezone);
1152            long beginMs = time.setJulianDay((int) rangeBegin);
1153            // We add one to lastDay because the time is set to 12am on the given
1154            // Julian day and we want to include all the events on the last day.
1155            long endMs = time.setJulianDay((int) rangeEnd + 1);
1156            // will lock the database.
1157            // we expand the instances here because we might be searching over
1158            // a range where instance expansion has not occurred yet
1159            acquireInstanceRange(beginMs, endMs,
1160                    true /* use minimum expansion window */,
1161                    false /* do not force Instances deletion and expansion */,
1162                    instancesTimezone,
1163                    isHomeTimezone
1164            );
1165            qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY);
1166        } else {
1167            // will lock the database.
1168            // we expand the instances here because we might be searching over
1169            // a range where instance expansion has not occurred yet
1170            acquireInstanceRange(rangeBegin, rangeEnd,
1171                    true /* use minimum expansion window */,
1172                    false /* do not force Instances deletion and expansion */,
1173                    instancesTimezone,
1174                    isHomeTimezone
1175            );
1176            qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN);
1177        }
1178
1179        return qb.query(mDb, projection, selection, selectionArgs,
1180                Instances._ID /* groupBy */, searchWhere /* having */, sort);
1181    }
1182
1183    private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end,
1184            String[] projection, String selection, String instancesTimezone,
1185            boolean isHomeTimezone) {
1186        qb.setTables(INSTANCE_QUERY_TABLES);
1187        qb.setProjectionMap(sInstancesProjectionMap);
1188        // Convert the first and last Julian day range to a range that uses
1189        // UTC milliseconds.
1190        Time time = new Time(instancesTimezone);
1191        long beginMs = time.setJulianDay(begin);
1192        // We add one to lastDay because the time is set to 12am on the given
1193        // Julian day and we want to include all the events on the last day.
1194        long endMs = time.setJulianDay(end + 1);
1195
1196        acquireInstanceRange(beginMs, endMs, true,
1197                false /* do not force Instances expansion */, instancesTimezone, isHomeTimezone);
1198        qb.appendWhere(SQL_WHERE_INSTANCES_BETWEEN_DAY);
1199        String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)};
1200
1201        return qb.query(mDb, projection, selection, selectionArgs,
1202                Instances.START_DAY /* groupBy */, null /* having */, null);
1203    }
1204
1205    /**
1206     * Ensure that the date range given has all elements in the instance
1207     * table.  Acquires the database lock and calls
1208     * {@link #acquireInstanceRangeLocked(long, long, boolean, boolean, String, boolean)}.
1209     *
1210     * @param begin start of range (ms)
1211     * @param end end of range (ms)
1212     * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
1213     * @param forceExpansion force the Instance deletion and expansion if set to true
1214     * @param instancesTimezone timezone we need to use for computing the instances
1215     * @param isHomeTimezone if true, we are in the "home" timezone
1216     */
1217    private void acquireInstanceRange(final long begin, final long end,
1218            final boolean useMinimumExpansionWindow, final boolean forceExpansion,
1219            final String instancesTimezone, final boolean isHomeTimezone) {
1220        mDb.beginTransaction();
1221        try {
1222            acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow,
1223                    forceExpansion, instancesTimezone, isHomeTimezone);
1224            mDb.setTransactionSuccessful();
1225        } finally {
1226            mDb.endTransaction();
1227        }
1228    }
1229
1230    /**
1231     * Ensure that the date range given has all elements in the instance
1232     * table.  The database lock must be held when calling this method.
1233     *
1234     * @param begin start of range (ms)
1235     * @param end end of range (ms)
1236     * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
1237     * @param forceExpansion force the Instance deletion and expansion if set to true
1238     * @param instancesTimezone timezone we need to use for computing the instances
1239     * @param isHomeTimezone if true, we are in the "home" timezone
1240     */
1241    void acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow,
1242            boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone) {
1243        long expandBegin = begin;
1244        long expandEnd = end;
1245
1246        if (DEBUG_INSTANCES) {
1247            Log.d(TAG + "-i", "acquireInstanceRange begin=" + begin + " end=" + end +
1248                    " useMin=" + useMinimumExpansionWindow + " force=" + forceExpansion);
1249        }
1250
1251        if (instancesTimezone == null) {
1252            Log.e(TAG, "Cannot run acquireInstanceRangeLocked() because instancesTimezone is null");
1253            return;
1254        }
1255
1256        if (useMinimumExpansionWindow) {
1257            // if we end up having to expand events into the instances table, expand
1258            // events for a minimal amount of time, so we do not have to perform
1259            // expansions frequently.
1260            long span = end - begin;
1261            if (span < MINIMUM_EXPANSION_SPAN) {
1262                long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
1263                expandBegin -= additionalRange;
1264                expandEnd += additionalRange;
1265            }
1266        }
1267
1268        // Check if the timezone has changed.
1269        // We do this check here because the database is locked and we can
1270        // safely delete all the entries in the Instances table.
1271        MetaData.Fields fields = mMetaData.getFieldsLocked();
1272        long maxInstance = fields.maxInstance;
1273        long minInstance = fields.minInstance;
1274        boolean timezoneChanged;
1275        if (isHomeTimezone) {
1276            String previousTimezone = mCalendarCache.readTimezoneInstancesPrevious();
1277            timezoneChanged = !instancesTimezone.equals(previousTimezone);
1278        } else {
1279            String localTimezone = TimeZone.getDefault().getID();
1280            timezoneChanged = !instancesTimezone.equals(localTimezone);
1281            // if we're in auto make sure we are using the device time zone
1282            if (timezoneChanged) {
1283                instancesTimezone = localTimezone;
1284            }
1285        }
1286        // if "home", then timezoneChanged only if current != previous
1287        // if "auto", then timezoneChanged, if !instancesTimezone.equals(localTimezone);
1288        if (maxInstance == 0 || timezoneChanged || forceExpansion) {
1289            if (DEBUG_INSTANCES) {
1290                Log.d(TAG + "-i", "Wiping instances and expanding from scratch");
1291            }
1292
1293            // Empty the Instances table and expand from scratch.
1294            mDb.execSQL("DELETE FROM " + Tables.INSTANCES + ";");
1295            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1296                Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances,"
1297                        + " timezone changed: " + timezoneChanged);
1298            }
1299            mInstancesHelper.expandInstanceRangeLocked(expandBegin, expandEnd, instancesTimezone);
1300
1301            mMetaData.writeLocked(instancesTimezone, expandBegin, expandEnd);
1302
1303            String timezoneType = mCalendarCache.readTimezoneType();
1304            // This may cause some double writes but guarantees the time zone in
1305            // the db and the time zone the instances are in is the same, which
1306            // future changes may affect.
1307            mCalendarCache.writeTimezoneInstances(instancesTimezone);
1308
1309            // If we're in auto check if we need to fix the previous tz value
1310            if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
1311                String prevTZ = mCalendarCache.readTimezoneInstancesPrevious();
1312                if (TextUtils.equals(TIMEZONE_GMT, prevTZ)) {
1313                    mCalendarCache.writeTimezoneInstancesPrevious(instancesTimezone);
1314                }
1315            }
1316            return;
1317        }
1318
1319        // If the desired range [begin, end] has already been
1320        // expanded, then simply return.  The range is inclusive, that is,
1321        // events that touch either endpoint are included in the expansion.
1322        // This means that a zero-duration event that starts and ends at
1323        // the endpoint will be included.
1324        // We use [begin, end] here and not [expandBegin, expandEnd] for
1325        // checking the range because a common case is for the client to
1326        // request successive days or weeks, for example.  If we checked
1327        // that the expanded range [expandBegin, expandEnd] then we would
1328        // always be expanding because there would always be one more day
1329        // or week that hasn't been expanded.
1330        if ((begin >= minInstance) && (end <= maxInstance)) {
1331            if (DEBUG_INSTANCES) {
1332                Log.d(TAG + "-i", "instances are already expanded");
1333            }
1334            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1335                Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
1336                        + ") falls within previously expanded range.");
1337            }
1338            return;
1339        }
1340
1341        // If the requested begin point has not been expanded, then include
1342        // more events than requested in the expansion (use "expandBegin").
1343        if (begin < minInstance) {
1344            mInstancesHelper.expandInstanceRangeLocked(expandBegin, minInstance, instancesTimezone);
1345            minInstance = expandBegin;
1346        }
1347
1348        // If the requested end point has not been expanded, then include
1349        // more events than requested in the expansion (use "expandEnd").
1350        if (end > maxInstance) {
1351            mInstancesHelper.expandInstanceRangeLocked(maxInstance, expandEnd, instancesTimezone);
1352            maxInstance = expandEnd;
1353        }
1354
1355        // Update the bounds on the Instances table.
1356        mMetaData.writeLocked(instancesTimezone, minInstance, maxInstance);
1357    }
1358
1359    @Override
1360    public String getType(Uri url) {
1361        int match = sUriMatcher.match(url);
1362        switch (match) {
1363            case EVENTS:
1364                return "vnd.android.cursor.dir/event";
1365            case EVENTS_ID:
1366                return "vnd.android.cursor.item/event";
1367            case REMINDERS:
1368                return "vnd.android.cursor.dir/reminder";
1369            case REMINDERS_ID:
1370                return "vnd.android.cursor.item/reminder";
1371            case CALENDAR_ALERTS:
1372                return "vnd.android.cursor.dir/calendar-alert";
1373            case CALENDAR_ALERTS_BY_INSTANCE:
1374                return "vnd.android.cursor.dir/calendar-alert-by-instance";
1375            case CALENDAR_ALERTS_ID:
1376                return "vnd.android.cursor.item/calendar-alert";
1377            case INSTANCES:
1378            case INSTANCES_BY_DAY:
1379            case EVENT_DAYS:
1380                return "vnd.android.cursor.dir/event-instance";
1381            case TIME:
1382                return "time/epoch";
1383            case PROVIDER_PROPERTIES:
1384                return "vnd.android.cursor.dir/property";
1385            default:
1386                throw new IllegalArgumentException("Unknown URL " + url);
1387        }
1388    }
1389
1390    /**
1391     * Determines if the event is recurrent, based on the provided values.
1392     */
1393    public static boolean isRecurrenceEvent(String rrule, String rdate, String originalId,
1394            String originalSyncId) {
1395        return (!TextUtils.isEmpty(rrule) ||
1396                !TextUtils.isEmpty(rdate) ||
1397                !TextUtils.isEmpty(originalId) ||
1398                !TextUtils.isEmpty(originalSyncId));
1399    }
1400
1401    /**
1402     * Takes an event and corrects the hrs, mins, secs if it is an allDay event.
1403     * <p>
1404     * AllDay events should have hrs, mins, secs set to zero. This checks if this is true and
1405     * corrects the fields DTSTART, DTEND, and DURATION if necessary.
1406     *
1407     * @param values The values to check and correct
1408     * @param modValues Any updates will be stored here.  This may be the same object as
1409     *   <strong>values</strong>.
1410     * @return Returns true if a correction was necessary, false otherwise
1411     */
1412    private boolean fixAllDayTime(ContentValues values, ContentValues modValues) {
1413        Integer allDayObj = values.getAsInteger(Events.ALL_DAY);
1414        if (allDayObj == null || allDayObj == 0) {
1415            return false;
1416        }
1417
1418        boolean neededCorrection = false;
1419
1420        Long dtstart = values.getAsLong(Events.DTSTART);
1421        Long dtend = values.getAsLong(Events.DTEND);
1422        String duration = values.getAsString(Events.DURATION);
1423        Time time = new Time();
1424        String tempValue;
1425
1426        // Change dtstart so h,m,s are 0 if necessary.
1427        time.clear(Time.TIMEZONE_UTC);
1428        time.set(dtstart.longValue());
1429        if (time.hour != 0 || time.minute != 0 || time.second != 0) {
1430            time.hour = 0;
1431            time.minute = 0;
1432            time.second = 0;
1433            modValues.put(Events.DTSTART, time.toMillis(true));
1434            neededCorrection = true;
1435        }
1436
1437        // If dtend exists for this event make sure it's h,m,s are 0.
1438        if (dtend != null) {
1439            time.clear(Time.TIMEZONE_UTC);
1440            time.set(dtend.longValue());
1441            if (time.hour != 0 || time.minute != 0 || time.second != 0) {
1442                time.hour = 0;
1443                time.minute = 0;
1444                time.second = 0;
1445                dtend = time.toMillis(true);
1446                modValues.put(Events.DTEND, dtend);
1447                neededCorrection = true;
1448            }
1449        }
1450
1451        if (duration != null) {
1452            int len = duration.length();
1453            /* duration is stored as either "P<seconds>S" or "P<days>D". This checks if it's
1454             * in the seconds format, and if so converts it to days.
1455             */
1456            if (len == 0) {
1457                duration = null;
1458            } else if (duration.charAt(0) == 'P' &&
1459                    duration.charAt(len - 1) == 'S') {
1460                int seconds = Integer.parseInt(duration.substring(1, len - 1));
1461                int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1462                duration = "P" + days + "D";
1463                modValues.put(Events.DURATION, duration);
1464                neededCorrection = true;
1465            }
1466        }
1467
1468        return neededCorrection;
1469    }
1470
1471
1472    /**
1473     * Determines whether the strings in the set name columns that may be overridden
1474     * when creating a recurring event exception.
1475     * <p>
1476     * This uses a white list because it screens out unknown columns and is a bit safer to
1477     * maintain than a black list.
1478     */
1479    private void checkAllowedInException(Set<String> keys) {
1480        for (String str : keys) {
1481            if (!ALLOWED_IN_EXCEPTION.contains(str.intern())) {
1482                throw new IllegalArgumentException("Exceptions can't overwrite " + str);
1483            }
1484        }
1485    }
1486
1487    /**
1488     * Splits a recurrent event at a specified instance.  This is useful when modifying "this
1489     * and all future events".
1490     *<p>
1491     * If the recurrence rule has a COUNT specified, we need to split that at the point of the
1492     * exception.  If the exception is instance N (0-based), the original COUNT is reduced
1493     * to N, and the exception's COUNT is set to (COUNT - N).
1494     *<p>
1495     * If the recurrence doesn't have a COUNT, we need to update or introduce an UNTIL value,
1496     * so that the original recurrence will end just before the exception instance.  (Note
1497     * that UNTIL dates are inclusive.)
1498     *<p>
1499     * This should not be used to update the first instance ("update all events" action).
1500     *
1501     * @param values The original event values; must include EVENT_TIMEZONE and DTSTART.
1502     *        The RRULE value may be modified (with the expectation that this will propagate
1503     *        into the exception event).
1504     * @param endTimeMillis The time before which the event must end (i.e. the start time of the
1505     *        exception event instance).
1506     * @return Values to apply to the original event.
1507     */
1508    private static ContentValues setRecurrenceEnd(ContentValues values, long endTimeMillis) {
1509        boolean origAllDay = values.getAsBoolean(Events.ALL_DAY);
1510        String origRrule = values.getAsString(Events.RRULE);
1511
1512        EventRecurrence origRecurrence = new EventRecurrence();
1513        origRecurrence.parse(origRrule);
1514
1515        // Get the start time of the first instance in the original recurrence.
1516        long startTimeMillis = values.getAsLong(Events.DTSTART);
1517        Time dtstart = new Time();
1518        dtstart.timezone = values.getAsString(Events.EVENT_TIMEZONE);
1519        dtstart.set(startTimeMillis);
1520
1521        ContentValues updateValues = new ContentValues();
1522
1523        if (origRecurrence.count > 0) {
1524            /*
1525             * Generate the full set of instances for this recurrence, from the first to the
1526             * one just before endTimeMillis.  The list should never be empty, because this method
1527             * should not be called for the first instance.  All we're really interested in is
1528             * the *number* of instances found.
1529             */
1530            RecurrenceSet recurSet = new RecurrenceSet(values);
1531            RecurrenceProcessor recurProc = new RecurrenceProcessor();
1532            long[] recurrences;
1533            try {
1534                recurrences = recurProc.expand(dtstart, recurSet, startTimeMillis, endTimeMillis);
1535            } catch (DateException de) {
1536                throw new RuntimeException(de);
1537            }
1538
1539            if (recurrences.length == 0) {
1540                throw new RuntimeException("can't use this method on first instance");
1541            }
1542
1543            EventRecurrence excepRecurrence = new EventRecurrence();
1544            excepRecurrence.parse(origRrule);  // TODO: add/use a copy constructor to EventRecurrence
1545            excepRecurrence.count -= recurrences.length;
1546            values.put(Events.RRULE, excepRecurrence.toString());
1547
1548            origRecurrence.count = recurrences.length;
1549
1550        } else {
1551            Time untilTime = new Time();
1552
1553            // The "until" time must be in UTC time in order for Google calendar
1554            // to display it properly. For all-day events, the "until" time string
1555            // must include just the date field, and not the time field. The
1556            // repeating events repeat up to and including the "until" time.
1557            untilTime.timezone = Time.TIMEZONE_UTC;
1558
1559            // Subtract one second from the exception begin time to get the "until" time.
1560            untilTime.set(endTimeMillis - 1000); // subtract one second (1000 millis)
1561            if (origAllDay) {
1562                untilTime.hour = untilTime.minute = untilTime.second = 0;
1563                untilTime.allDay = true;
1564                untilTime.normalize(false);
1565
1566                // This should no longer be necessary -- DTSTART should already be in the correct
1567                // format for an all-day event.
1568                dtstart.hour = dtstart.minute = dtstart.second = 0;
1569                dtstart.allDay = true;
1570                dtstart.timezone = Time.TIMEZONE_UTC;
1571            }
1572            origRecurrence.until = untilTime.format2445();
1573        }
1574
1575        updateValues.put(Events.RRULE, origRecurrence.toString());
1576        updateValues.put(Events.DTSTART, dtstart.normalize(true));
1577        return updateValues;
1578    }
1579
1580    /**
1581     * Handles insertion of an exception to a recurring event.
1582     * <p>
1583     * There are two modes, selected based on the presence of "rrule" in modValues:
1584     * <ol>
1585     * <li> Create a single instance exception ("modify current event only").
1586     * <li> Cap the original event, and create a new recurring event ("modify this and all
1587     * future events").
1588     * </ol>
1589     * This may be used for "modify all instances of the event" by simply selecting the
1590     * very first instance as the exception target.  In that case, the ID of the "new"
1591     * exception event will be the same as the originalEventId.
1592     *
1593     * @param originalEventId The _id of the event to be modified
1594     * @param modValues Event columns to update
1595     * @param callerIsSyncAdapter Set if the content provider client is the sync adapter
1596     * @return the ID of the new "exception" event, or -1 on failure
1597     */
1598    private long handleInsertException(long originalEventId, ContentValues modValues,
1599            boolean callerIsSyncAdapter) {
1600        if (DEBUG_EXCEPTION) {
1601            Log.i(TAG, "RE: values: " + modValues.toString());
1602        }
1603
1604        // Make sure they have specified an instance via originalInstanceTime.
1605        Long originalInstanceTime = modValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1606        if (originalInstanceTime == null) {
1607            throw new IllegalArgumentException("Exceptions must specify " +
1608                    Events.ORIGINAL_INSTANCE_TIME);
1609        }
1610
1611        // Check for attempts to override values that shouldn't be touched.
1612        checkAllowedInException(modValues.keySet());
1613
1614        // If this isn't the sync adapter, set the "dirty" flag in any Event we modify.
1615        if (!callerIsSyncAdapter) {
1616            modValues.put(Events.DIRTY, true);
1617        }
1618
1619        // Wrap all database accesses in a transaction.
1620        mDb.beginTransaction();
1621        Cursor cursor = null;
1622        try {
1623            // TODO: verify that there's an instance corresponding to the specified time
1624            //       (does this matter? it's weird, but not fatal?)
1625
1626            // Grab the full set of columns for this event.
1627            cursor = mDb.query(Tables.EVENTS, null /* columns */,
1628                    SQL_WHERE_ID, new String[] { String.valueOf(originalEventId) },
1629                    null /* groupBy */, null /* having */, null /* sortOrder */);
1630            if (cursor.getCount() != 1) {
1631                Log.e(TAG, "Original event ID " + originalEventId + " lookup failed (count is " +
1632                        cursor.getCount() + ")");
1633                return -1;
1634            }
1635            //DatabaseUtils.dumpCursor(cursor);
1636
1637            /*
1638             * Verify that the original event is in fact a recurring event by checking for the
1639             * presence of an RRULE.  If it's there, we assume that the event is otherwise
1640             * properly constructed (e.g. no DTEND).
1641             */
1642            cursor.moveToFirst();
1643            int rruleCol = cursor.getColumnIndex(Events.RRULE);
1644            if (TextUtils.isEmpty(cursor.getString(rruleCol))) {
1645                Log.e(TAG, "Original event has no rrule");
1646                return -1;
1647            }
1648            if (DEBUG_EXCEPTION) {
1649                Log.d(TAG, "RE: old RRULE is " + cursor.getString(rruleCol));
1650            }
1651
1652            // Verify that the original event is not itself a (single-instance) exception.
1653            int originalIdCol = cursor.getColumnIndex(Events.ORIGINAL_ID);
1654            if (!TextUtils.isEmpty(cursor.getString(originalIdCol))) {
1655                Log.e(TAG, "Original event is an exception");
1656                return -1;
1657            }
1658
1659            boolean createSingleException = TextUtils.isEmpty(modValues.getAsString(Events.RRULE));
1660
1661            // TODO: check for the presence of an existing exception on this event+instance?
1662            //       The caller should be modifying that, not creating another exception.
1663            //       (Alternatively, we could do that for them.)
1664
1665            // Create a new ContentValues for the new event.  Start with the original event,
1666            // and drop in the new caller-supplied values.  This will set originalInstanceTime.
1667            ContentValues values = new ContentValues();
1668            DatabaseUtils.cursorRowToContentValues(cursor, values);
1669
1670            // TODO: if we're changing this to an all-day event, we should ensure that
1671            //       hours/mins/secs on DTSTART are zeroed out (before computing DTEND).
1672            //       See fixAllDayTime().
1673
1674            boolean createNewEvent = true;
1675            if (createSingleException) {
1676                /*
1677                 * Save a copy of a few fields that will migrate to new places.
1678                 */
1679                String _id = values.getAsString(Events._ID);
1680                String _sync_id = values.getAsString(Events._SYNC_ID);
1681                boolean allDay = values.getAsBoolean(Events.ALL_DAY);
1682
1683                /*
1684                 * Wipe out some fields that we don't want to clone into the exception event.
1685                 */
1686                for (String str : DONT_CLONE_INTO_EXCEPTION) {
1687                    values.remove(str);
1688                }
1689
1690                /*
1691                 * Merge the new values on top of the existing values.  Note this sets
1692                 * originalInstanceTime.
1693                 */
1694                values.putAll(modValues);
1695
1696                /*
1697                 * Copy some fields to their "original" counterparts:
1698                 *   _id --> original_id
1699                 *   _sync_id --> original_sync_id
1700                 *   allDay --> originalAllDay
1701                 *
1702                 * If this event hasn't been sync'ed with the server yet, the _sync_id field will
1703                 * be null.  We will need to fill original_sync_id in later.  (May not be able to
1704                 * do it right when our own _sync_id field gets populated, because the order of
1705                 * events from the server may not be what we want -- could update the exception
1706                 * before updating the original event.)
1707                 *
1708                 * _id is removed later (right before we write the event).
1709                 */
1710                values.put(Events.ORIGINAL_ID, _id);
1711                values.put(Events.ORIGINAL_SYNC_ID, _sync_id);
1712                values.put(Events.ORIGINAL_ALL_DAY, allDay);
1713
1714                // Mark the exception event status as "tentative", unless the caller has some
1715                // other value in mind (like STATUS_CANCELED).
1716                if (!values.containsKey(Events.STATUS)) {
1717                    values.put(Events.STATUS, Events.STATUS_TENTATIVE);
1718                }
1719
1720                // We're converting from recurring to non-recurring.  Clear out RRULE and replace
1721                // DURATION with DTEND.
1722                values.remove(Events.RRULE);
1723
1724                Duration duration = new Duration();
1725                String durationStr = values.getAsString(Events.DURATION);
1726                try {
1727                    duration.parse(durationStr);
1728                } catch (Exception ex) {
1729                    // NullPointerException if the original event had no duration.
1730                    // DateException if the duration was malformed.
1731                    Log.w(TAG, "Bad duration in recurring event: " + durationStr, ex);
1732                    return -1;
1733                }
1734
1735                /*
1736                 * We want to compute DTEND as an offset from the start time of the instance.
1737                 * If the caller specified a new value for DTSTART, we want to use that; if not,
1738                 * the DTSTART in "values" will be the start time of the first instance in the
1739                 * recurrence, so we want to replace it with ORIGINAL_INSTANCE_TIME.
1740                 */
1741                long start;
1742                if (modValues.containsKey(Events.DTSTART)) {
1743                    start = values.getAsLong(Events.DTSTART);
1744                } else {
1745                    start = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1746                    values.put(Events.DTSTART, start);
1747                }
1748                values.put(Events.DTEND, start + duration.getMillis());
1749                if (DEBUG_EXCEPTION) {
1750                    Log.d(TAG, "RE: ORIG_INST_TIME=" + start +
1751                            ", duration=" + duration.getMillis() +
1752                            ", generated DTEND=" + values.getAsLong(Events.DTEND));
1753                }
1754                values.remove(Events.DURATION);
1755            } else {
1756                /*
1757                 * We're going to "split" the recurring event, making the old one stop before
1758                 * this instance, and creating a new recurring event that starts here.
1759                 *
1760                 * No need to fill out the "original" fields -- the new event is not tied to
1761                 * the previous event in any way.
1762                 *
1763                 * If this is the first event in the series, we can just update the existing
1764                 * event with the values.
1765                 */
1766                boolean canceling = (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED);
1767
1768                if (originalInstanceTime.equals(values.getAsLong(Events.DTSTART))) {
1769                    /*
1770                     * Update fields in the existing event.  Rather than use the merged data
1771                     * from the cursor, we just do the update with the new value set after
1772                     * removing the ORIGINAL_INSTANCE_TIME entry.
1773                     */
1774                    if (canceling) {
1775                        // TODO: should we just call deleteEventInternal?
1776                        Log.d(TAG, "Note: canceling entire event via exception call");
1777                    }
1778                    if (DEBUG_EXCEPTION) {
1779                        Log.d(TAG, "RE: updating full event");
1780                    }
1781                    if (!validateRecurrenceRule(modValues)) {
1782                        throw new IllegalArgumentException("Invalid recurrence rule: " +
1783                                values.getAsString(Events.RRULE));
1784                    }
1785                    modValues.remove(Events.ORIGINAL_INSTANCE_TIME);
1786                    mDb.update(Tables.EVENTS, modValues, SQL_WHERE_ID,
1787                            new String[] { Long.toString(originalEventId) });
1788                    createNewEvent = false; // skip event creation and related-table cloning
1789                } else {
1790                    if (DEBUG_EXCEPTION) {
1791                        Log.d(TAG, "RE: splitting event");
1792                    }
1793
1794                    /*
1795                     * Cap the original event so it ends just before the target instance.  In
1796                     * some cases (nonzero COUNT) this will also update the RRULE in "values",
1797                     * so that the exception we're creating terminates appropriately.  If a
1798                     * new RRULE was specified by the caller, the new rule will overwrite our
1799                     * changes when we merge the new values in below (which is the desired
1800                     * behavior).
1801                     */
1802                    ContentValues splitValues = setRecurrenceEnd(values, originalInstanceTime);
1803                    mDb.update(Tables.EVENTS, splitValues, SQL_WHERE_ID,
1804                            new String[] { Long.toString(originalEventId) });
1805
1806                    /*
1807                     * Prepare the new event.  We remove originalInstanceTime, because we're now
1808                     * creating a new event rather than an exception.
1809                     *
1810                     * We're always cloning a non-exception event (we tested to make sure the
1811                     * event doesn't specify original_id, and we don't allow original_id in the
1812                     * modValues), so we shouldn't end up creating a new event that looks like
1813                     * an exception.
1814                     */
1815                    values.putAll(modValues);
1816                    values.remove(Events.ORIGINAL_INSTANCE_TIME);
1817                }
1818            }
1819
1820            long newEventId;
1821            if (createNewEvent) {
1822                values.remove(Events._ID);      // don't try to set this explicitly
1823                if (callerIsSyncAdapter) {
1824                    scrubEventData(values, null);
1825                } else {
1826                    validateEventData(values);
1827                }
1828
1829                newEventId = mDb.insert(Tables.EVENTS, null, values);
1830                if (newEventId < 0) {
1831                    Log.w(TAG, "Unable to add exception to recurring event");
1832                    Log.w(TAG, "Values: " + values);
1833                    return -1;
1834                }
1835                if (DEBUG_EXCEPTION) {
1836                    Log.d(TAG, "RE: new ID is " + newEventId);
1837                }
1838
1839                // TODO: do we need to do something like this?
1840                //updateEventRawTimesLocked(id, updatedValues);
1841
1842                /*
1843                 * Force re-computation of the Instances associated with the recurrence event.
1844                 */
1845                mInstancesHelper.updateInstancesLocked(values, newEventId, true, mDb);
1846
1847                /*
1848                 * Some of the other tables (Attendees, Reminders, ExtendedProperties) reference
1849                 * the Event ID.  We need to copy the entries from the old event, filling in the
1850                 * new event ID, so that somebody doing a SELECT on those tables will find
1851                 * matching entries.
1852                 */
1853                CalendarDatabaseHelper.copyEventRelatedTables(mDb, newEventId, originalEventId);
1854
1855                /*
1856                 * If we modified Event.selfAttendeeStatus, we need to keep the corresponding
1857                 * entry in the Attendees table in sync.
1858                 */
1859                if (modValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
1860                    /*
1861                     * Each Attendee is identified by email address.  To find the entry that
1862                     * corresponds to "self", we want to compare that address to the owner of
1863                     * the Calendar.  We're expecting to find one matching entry in Attendees.
1864                     */
1865                    long calendarId = values.getAsLong(Events.CALENDAR_ID);
1866                    cursor = mDb.query(Tables.CALENDARS, new String[] { Calendars.OWNER_ACCOUNT },
1867                            SQL_WHERE_ID, new String[] { String.valueOf(calendarId) },
1868                            null /* groupBy */, null /* having */, null /* sortOrder */);
1869                    if (!cursor.moveToFirst()) {
1870                        Log.w(TAG, "Can't get calendar account_name for calendar " + calendarId);
1871                    } else {
1872                        String accountName = cursor.getString(0);
1873                        ContentValues attValues = new ContentValues();
1874                        attValues.put(Attendees.ATTENDEE_STATUS,
1875                                modValues.getAsString(Events.SELF_ATTENDEE_STATUS));
1876
1877                        if (DEBUG_EXCEPTION) {
1878                            Log.d(TAG, "Updating attendee status for event=" + newEventId +
1879                                    " name=" + accountName + " to " +
1880                                    attValues.getAsString(Attendees.ATTENDEE_STATUS));
1881                        }
1882                        int count = mDb.update(Tables.ATTENDEES, attValues,
1883                                Attendees.EVENT_ID + "=? AND " + Attendees.ATTENDEE_EMAIL + "=?",
1884                                new String[] { String.valueOf(newEventId), accountName });
1885                        if (count != 1 && count != 2) {
1886                            // We're only expecting one matching entry.  We might briefly see
1887                            // two during a server sync.
1888                            Log.e(TAG, "Attendee status update on event=" + newEventId +
1889                                    " name=" + accountName + " touched " + count + " rows");
1890                            if (false) {
1891                                // This dumps PII in the log, don't ship with it enabled.
1892                                Cursor debugCursor = mDb.query(Tables.ATTENDEES, null,
1893                                        Attendees.EVENT_ID + "=? AND " +
1894                                            Attendees.ATTENDEE_EMAIL + "=?",
1895                                        new String[] { String.valueOf(newEventId), accountName },
1896                                        null, null, null);
1897                                DatabaseUtils.dumpCursor(debugCursor);
1898                            }
1899                            throw new RuntimeException("Status update WTF");
1900                        }
1901                    }
1902                    cursor.close();
1903                }
1904            } else {
1905                /*
1906                 * Update any Instances changed by the update to this Event.
1907                 */
1908                mInstancesHelper.updateInstancesLocked(values, originalEventId, false, mDb);
1909                newEventId = originalEventId;
1910            }
1911
1912            mDb.setTransactionSuccessful();
1913            return newEventId;
1914        } finally {
1915            if (cursor != null) {
1916                cursor.close();
1917            }
1918            mDb.endTransaction();
1919        }
1920    }
1921
1922    @Override
1923    protected Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
1924        if (Log.isLoggable(TAG, Log.VERBOSE)) {
1925            Log.v(TAG, "insertInTransaction: " + uri);
1926        }
1927        final int match = sUriMatcher.match(uri);
1928        verifyTransactionAllowed(TRANSACTION_INSERT, uri, values, callerIsSyncAdapter, match,
1929                null /* selection */, null /* selection args */);
1930
1931        long id = 0;
1932
1933        switch (match) {
1934            case SYNCSTATE:
1935                id = mDbHelper.getSyncState().insert(mDb, values);
1936                break;
1937            case EVENTS:
1938                if (!callerIsSyncAdapter) {
1939                    values.put(Events.DIRTY, 1);
1940                }
1941                if (!values.containsKey(Events.DTSTART)) {
1942                    throw new RuntimeException("DTSTART field missing from event");
1943                }
1944                // TODO: do we really need to make a copy?
1945                ContentValues updatedValues = new ContentValues(values);
1946                if (callerIsSyncAdapter) {
1947                    scrubEventData(updatedValues, null);
1948                } else {
1949                    validateEventData(updatedValues);
1950                }
1951                // updateLastDate must be after validation, to ensure proper last date computation
1952                updatedValues = updateLastDate(updatedValues);
1953                if (updatedValues == null) {
1954                    throw new RuntimeException("Could not insert event.");
1955                    // return null;
1956                }
1957                String owner = null;
1958                if (updatedValues.containsKey(Events.CALENDAR_ID) &&
1959                        !updatedValues.containsKey(Events.ORGANIZER)) {
1960                    owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
1961                    // TODO: This isn't entirely correct.  If a guest is adding a recurrence
1962                    // exception to an event, the organizer should stay the original organizer.
1963                    // This value doesn't go to the server and it will get fixed on sync,
1964                    // so it shouldn't really matter.
1965                    if (owner != null) {
1966                        updatedValues.put(Events.ORGANIZER, owner);
1967                    }
1968                }
1969                if (updatedValues.containsKey(Events.ORIGINAL_SYNC_ID)
1970                        && !updatedValues.containsKey(Events.ORIGINAL_ID)) {
1971                    long originalId = getOriginalId(updatedValues
1972                            .getAsString(Events.ORIGINAL_SYNC_ID));
1973                    if (originalId != -1) {
1974                        updatedValues.put(Events.ORIGINAL_ID, originalId);
1975                    }
1976                } else if (!updatedValues.containsKey(Events.ORIGINAL_SYNC_ID)
1977                        && updatedValues.containsKey(Events.ORIGINAL_ID)) {
1978                    String originalSyncId = getOriginalSyncId(updatedValues
1979                            .getAsLong(Events.ORIGINAL_ID));
1980                    if (!TextUtils.isEmpty(originalSyncId)) {
1981                        updatedValues.put(Events.ORIGINAL_SYNC_ID, originalSyncId);
1982                    }
1983                }
1984                if (fixAllDayTime(updatedValues, updatedValues)) {
1985                    if (Log.isLoggable(TAG, Log.WARN)) {
1986                        Log.w(TAG, "insertInTransaction: " +
1987                                "allDay is true but sec, min, hour were not 0.");
1988                    }
1989                }
1990                // Insert the row
1991                id = mDbHelper.eventsInsert(updatedValues);
1992                if (id != -1) {
1993                    updateEventRawTimesLocked(id, updatedValues);
1994                    mInstancesHelper.updateInstancesLocked(updatedValues, id,
1995                            true /* new event */, mDb);
1996
1997                    // If we inserted a new event that specified the self-attendee
1998                    // status, then we need to add an entry to the attendees table.
1999                    if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
2000                        int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS);
2001                        if (owner == null) {
2002                            owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
2003                        }
2004                        createAttendeeEntry(id, status, owner);
2005                    }
2006                    // if the Event Timezone is defined, store it as the original one in the
2007                    // ExtendedProperties table
2008                    if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) {
2009                        String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE);
2010
2011                        ContentValues expropsValues = new ContentValues();
2012                        expropsValues.put(CalendarContract.ExtendedProperties.EVENT_ID, id);
2013                        expropsValues.put(CalendarContract.ExtendedProperties.NAME,
2014                                EXT_PROP_ORIGINAL_TIMEZONE);
2015                        expropsValues.put(CalendarContract.ExtendedProperties.VALUE,
2016                                originalTimezone);
2017
2018                        // Insert the extended property
2019                        long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues);
2020                        if (exPropId == -1) {
2021                            if (Log.isLoggable(TAG, Log.ERROR)) {
2022                                Log.e(TAG, "Cannot add the original Timezone in the "
2023                                        + "ExtendedProperties table for Event: " + id);
2024                            }
2025                        } else {
2026                            // Update the Event for saying it has some extended properties
2027                            ContentValues eventValues = new ContentValues();
2028                            eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1");
2029                            int result = mDb.update("Events", eventValues, SQL_WHERE_ID,
2030                                    new String[] {String.valueOf(id)});
2031                            if (result <= 0) {
2032                                if (Log.isLoggable(TAG, Log.ERROR)) {
2033                                    Log.e(TAG, "Cannot update hasExtendedProperties column"
2034                                            + " for Event: " + id);
2035                                }
2036                            }
2037                        }
2038                    }
2039
2040                    /*
2041                     * The server might send exceptions before the event they refer to.  When
2042                     * this happens, the originalId field will not have been set in the
2043                     * exception events (it's the recurrence events' _id field, so it can't be
2044                     * known until the recurrence event is created).  When we add a recurrence
2045                     * event with a non-empty _sync_id field, we write that event's _id to the
2046                     * originalId field of any events whose originalSyncId matches _sync_id.
2047                     */
2048                    if (values.containsKey(Events._SYNC_ID) && values.containsKey(Events.RRULE)
2049                            && !TextUtils.isEmpty(values.getAsString(Events.RRULE))) {
2050                        String syncId = values.getAsString(Events._SYNC_ID);
2051                        if (TextUtils.isEmpty(syncId)) {
2052                            break;
2053                        }
2054                        ContentValues originalValues = new ContentValues();
2055                        originalValues.put(Events.ORIGINAL_ID, id);
2056                        mDb.update(Tables.EVENTS, originalValues, Events.ORIGINAL_SYNC_ID + "=?",
2057                                new String[] {syncId});
2058                    }
2059                    sendUpdateNotification(id, callerIsSyncAdapter);
2060                }
2061                break;
2062            case EXCEPTION_ID:
2063                long originalEventId = ContentUris.parseId(uri);
2064                id = handleInsertException(originalEventId, values, callerIsSyncAdapter);
2065                break;
2066            case CALENDARS:
2067                Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
2068                if (syncEvents != null && syncEvents == 1) {
2069                    String accountName = values.getAsString(Calendars.ACCOUNT_NAME);
2070                    String accountType = values.getAsString(
2071                            Calendars.ACCOUNT_TYPE);
2072                    final Account account = new Account(accountName, accountType);
2073                    String eventsUrl = values.getAsString(Calendars.CAL_SYNC1);
2074                    mDbHelper.scheduleSync(account, false /* two-way sync */, eventsUrl);
2075                }
2076                id = mDbHelper.calendarsInsert(values);
2077                sendUpdateNotification(id, callerIsSyncAdapter);
2078                break;
2079            case ATTENDEES:
2080                if (!values.containsKey(Attendees.EVENT_ID)) {
2081                    throw new IllegalArgumentException("Attendees values must "
2082                            + "contain an event_id");
2083                }
2084                if (!callerIsSyncAdapter) {
2085                    final Long eventId = values.getAsLong(Attendees.EVENT_ID);
2086                    mDbHelper.duplicateEvent(eventId);
2087                    setEventDirty(eventId);
2088                }
2089                id = mDbHelper.attendeesInsert(values);
2090
2091                // Copy the attendee status value to the Events table.
2092                updateEventAttendeeStatus(mDb, values);
2093                break;
2094            case REMINDERS:
2095                if (!values.containsKey(Reminders.EVENT_ID)) {
2096                    throw new IllegalArgumentException("Reminders values must "
2097                            + "contain an event_id");
2098                }
2099                if (!callerIsSyncAdapter) {
2100                    final Long eventId = values.getAsLong(Reminders.EVENT_ID);
2101                    mDbHelper.duplicateEvent(eventId);
2102                    setEventDirty(eventId);
2103                }
2104                id = mDbHelper.remindersInsert(values);
2105
2106                // Schedule another event alarm, if necessary
2107                if (Log.isLoggable(TAG, Log.DEBUG)) {
2108                    Log.d(TAG, "insertInternal() changing reminder");
2109                }
2110                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
2111                break;
2112            case CALENDAR_ALERTS:
2113                if (!values.containsKey(CalendarAlerts.EVENT_ID)) {
2114                    throw new IllegalArgumentException("CalendarAlerts values must "
2115                            + "contain an event_id");
2116                }
2117                id = mDbHelper.calendarAlertsInsert(values);
2118                // Note: dirty bit is not set for Alerts because it is not synced.
2119                // It is generated from Reminders, which is synced.
2120                break;
2121            case EXTENDED_PROPERTIES:
2122                if (!values.containsKey(CalendarContract.ExtendedProperties.EVENT_ID)) {
2123                    throw new IllegalArgumentException("ExtendedProperties values must "
2124                            + "contain an event_id");
2125                }
2126                if (!callerIsSyncAdapter) {
2127                    final Long eventId = values
2128                            .getAsLong(CalendarContract.ExtendedProperties.EVENT_ID);
2129                    mDbHelper.duplicateEvent(eventId);
2130                    setEventDirty(eventId);
2131                }
2132                id = mDbHelper.extendedPropertiesInsert(values);
2133                break;
2134            case EVENTS_ID:
2135            case REMINDERS_ID:
2136            case CALENDAR_ALERTS_ID:
2137            case EXTENDED_PROPERTIES_ID:
2138            case INSTANCES:
2139            case INSTANCES_BY_DAY:
2140            case EVENT_DAYS:
2141            case PROVIDER_PROPERTIES:
2142                throw new UnsupportedOperationException("Cannot insert into that URL: " + uri);
2143            default:
2144                throw new IllegalArgumentException("Unknown URL " + uri);
2145        }
2146
2147        if (id < 0) {
2148            return null;
2149        }
2150
2151        return ContentUris.withAppendedId(uri, id);
2152    }
2153
2154    /**
2155     * Validates the recurrence rule, if any.  We allow single- and multi-rule RRULEs.
2156     * <p>
2157     * TODO: Validate RDATE, EXRULE, EXDATE (possibly passing in an indication of whether we
2158     * believe we have the full set, so we can reject EXRULE when not accompanied by RRULE).
2159     *
2160     * @return A boolean indicating successful validation.
2161     */
2162    private boolean validateRecurrenceRule(ContentValues values) {
2163        String rrule = values.getAsString(Events.RRULE);
2164
2165        if (!TextUtils.isEmpty(rrule)) {
2166            String[] ruleList = rrule.split("\n");
2167            for (String recur : ruleList) {
2168                EventRecurrence er = new EventRecurrence();
2169                try {
2170                    er.parse(recur);
2171                } catch (EventRecurrence.InvalidFormatException ife) {
2172                    Log.w(TAG, "Invalid recurrence rule: " + recur);
2173                    return false;
2174                }
2175            }
2176        }
2177
2178        return true;
2179    }
2180
2181    /**
2182     * Do some scrubbing on event data before inserting or updating. In particular make
2183     * dtend, duration, etc make sense for the type of event (regular, recurrence, exception).
2184     * Remove any unexpected fields.
2185     *
2186     * @param values the ContentValues to insert.
2187     * @param modValues if non-null, explicit null entries will be added here whenever something
2188     *   is removed from <strong>values</strong>.
2189     */
2190    private void scrubEventData(ContentValues values, ContentValues modValues) {
2191        boolean hasDtend = values.getAsLong(Events.DTEND) != null;
2192        boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION));
2193        boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE));
2194        boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE));
2195        boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_SYNC_ID));
2196        boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null;
2197        if (hasRrule || hasRdate) {
2198            // Recurrence:
2199            // dtstart is start time of first event
2200            // dtend is null
2201            // duration is the duration of the event
2202            // rrule is a valid recurrence rule
2203            // lastDate is the end of the last event or null if it repeats forever
2204            // originalEvent is null
2205            // originalInstanceTime is null
2206            if (!validateRecurrenceRule(values)) {
2207                throw new IllegalArgumentException("Invalid recurrence rule: " +
2208                        values.getAsString(Events.RRULE));
2209            }
2210            if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) {
2211                Log.d(TAG, "Scrubbing DTEND, ORIGINAL_SYNC_ID, ORIGINAL_INSTANCE_TIME");
2212                if (Log.isLoggable(TAG, Log.DEBUG)) {
2213                    Log.d(TAG, "Invalid values for recurrence: " + values);
2214                }
2215                values.remove(Events.DTEND);
2216                values.remove(Events.ORIGINAL_SYNC_ID);
2217                values.remove(Events.ORIGINAL_INSTANCE_TIME);
2218                if (modValues != null) {
2219                    modValues.putNull(Events.DTEND);
2220                    modValues.putNull(Events.ORIGINAL_SYNC_ID);
2221                    modValues.putNull(Events.ORIGINAL_INSTANCE_TIME);
2222                }
2223            }
2224        } else if (hasOriginalEvent || hasOriginalInstanceTime) {
2225            // Recurrence exception
2226            // dtstart is start time of exception event
2227            // dtend is end time of exception event
2228            // duration is null
2229            // rrule is null
2230            // lastdate is same as dtend
2231            // originalEvent is the _sync_id of the recurrence
2232            // originalInstanceTime is the start time of the event being replaced
2233            if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) {
2234                Log.d(TAG, "Scrubbing DURATION");
2235                if (Log.isLoggable(TAG, Log.DEBUG)) {
2236                    Log.d(TAG, "Invalid values for recurrence exception: " + values);
2237                }
2238                values.remove(Events.DURATION);
2239                if (modValues != null) {
2240                    modValues.putNull(Events.DURATION);
2241                }
2242            }
2243        } else {
2244            // Regular event
2245            // dtstart is the start time
2246            // dtend is the end time
2247            // duration is null
2248            // rrule is null
2249            // lastDate is the same as dtend
2250            // originalEvent is null
2251            // originalInstanceTime is null
2252            if (!hasDtend || hasDuration) {
2253                Log.d(TAG, "Scrubbing DURATION");
2254                if (Log.isLoggable(TAG, Log.DEBUG)) {
2255                    Log.d(TAG, "Invalid values for event: " + values);
2256                }
2257                values.remove(Events.DURATION);
2258                if (modValues != null) {
2259                    modValues.putNull(Events.DURATION);
2260                }
2261            }
2262        }
2263    }
2264
2265    /**
2266     * Validates event data.  Pass in the full set of values for the event (i.e. not just
2267     * a part that's being updated).
2268     *
2269     * @param values Event data.
2270     * @throws IllegalArgumentException if bad data is found.
2271     */
2272    private void validateEventData(ContentValues values) {
2273        boolean hasDtstart = values.getAsLong(Events.DTSTART) != null;
2274        boolean hasDtend = values.getAsLong(Events.DTEND) != null;
2275        boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION));
2276        boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE));
2277        boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE));
2278        boolean hasCalId = !TextUtils.isEmpty(values.getAsString(Events.CALENDAR_ID));
2279        if (!hasCalId) {
2280            throw new IllegalArgumentException("New events must include a calendar_id.");
2281        }
2282        if (hasRrule || hasRdate) {
2283            if (!validateRecurrenceRule(values)) {
2284                throw new IllegalArgumentException("Invalid recurrence rule: " +
2285                        values.getAsString(Events.RRULE));
2286            }
2287        }
2288
2289        if (!hasDtstart) {
2290            throw new IllegalArgumentException("DTSTART cannot be empty.");
2291        }
2292        if (!hasDuration && !hasDtend) {
2293            throw new IllegalArgumentException("DTEND and DURATION cannot both be null for " +
2294                    "an event.");
2295        }
2296        if (hasDuration && hasDtend) {
2297            throw new IllegalArgumentException("Cannot have both DTEND and DURATION in an event");
2298        }
2299    }
2300
2301    private void setEventDirty(long eventId) {
2302        mDb.execSQL(SQL_UPDATE_EVENT_SET_DIRTY, new Object[] {eventId});
2303    }
2304
2305    private long getOriginalId(String originalSyncId) {
2306        if (TextUtils.isEmpty(originalSyncId)) {
2307            return -1;
2308        }
2309        // Get the original id for this event
2310        long originalId = -1;
2311        Cursor c = null;
2312        try {
2313            c = query(Events.CONTENT_URI, ID_ONLY_PROJECTION,
2314                    Events._SYNC_ID + "=?", new String[] {originalSyncId}, null);
2315            if (c != null && c.moveToFirst()) {
2316                originalId = c.getLong(0);
2317            }
2318        } finally {
2319            if (c != null) {
2320                c.close();
2321            }
2322        }
2323        return originalId;
2324    }
2325
2326    private String getOriginalSyncId(long originalId) {
2327        if (originalId == -1) {
2328            return null;
2329        }
2330        // Get the original id for this event
2331        String originalSyncId = null;
2332        Cursor c = null;
2333        try {
2334            c = query(Events.CONTENT_URI, new String[] {Events._SYNC_ID},
2335                    Events._ID + "=?", new String[] {Long.toString(originalId)}, null);
2336            if (c != null && c.moveToFirst()) {
2337                originalSyncId = c.getString(0);
2338            }
2339        } finally {
2340            if (c != null) {
2341                c.close();
2342            }
2343        }
2344        return originalSyncId;
2345    }
2346
2347    /**
2348     * Gets the calendar's owner for an event.
2349     * @param calId
2350     * @return email of owner or null
2351     */
2352    private String getOwner(long calId) {
2353        if (calId < 0) {
2354            if (Log.isLoggable(TAG, Log.ERROR)) {
2355                Log.e(TAG, "Calendar Id is not valid: " + calId);
2356            }
2357            return null;
2358        }
2359        // Get the email address of this user from this Calendar
2360        String emailAddress = null;
2361        Cursor cursor = null;
2362        try {
2363            cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2364                    new String[] { Calendars.OWNER_ACCOUNT },
2365                    null /* selection */,
2366                    null /* selectionArgs */,
2367                    null /* sort */);
2368            if (cursor == null || !cursor.moveToFirst()) {
2369                if (Log.isLoggable(TAG, Log.DEBUG)) {
2370                    Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2371                }
2372                return null;
2373            }
2374            emailAddress = cursor.getString(0);
2375        } finally {
2376            if (cursor != null) {
2377                cursor.close();
2378            }
2379        }
2380        return emailAddress;
2381    }
2382
2383    /**
2384     * Creates an entry in the Attendees table that refers to the given event
2385     * and that has the given response status.
2386     *
2387     * @param eventId the event id that the new entry in the Attendees table
2388     * should refer to
2389     * @param status the response status
2390     * @param emailAddress the email of the attendee
2391     */
2392    private void createAttendeeEntry(long eventId, int status, String emailAddress) {
2393        ContentValues values = new ContentValues();
2394        values.put(Attendees.EVENT_ID, eventId);
2395        values.put(Attendees.ATTENDEE_STATUS, status);
2396        values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
2397        // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
2398        // on sync.
2399        values.put(Attendees.ATTENDEE_RELATIONSHIP,
2400                Attendees.RELATIONSHIP_ATTENDEE);
2401        values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
2402
2403        // We don't know the ATTENDEE_NAME but that will be filled in by the
2404        // server and sent back to us.
2405        mDbHelper.attendeesInsert(values);
2406    }
2407
2408    /**
2409     * Updates the attendee status in the Events table to be consistent with
2410     * the value in the Attendees table.
2411     *
2412     * @param db the database
2413     * @param attendeeValues the column values for one row in the Attendees
2414     * table.
2415     */
2416    private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
2417        // Get the event id for this attendee
2418        long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
2419
2420        if (MULTIPLE_ATTENDEES_PER_EVENT) {
2421            // Get the calendar id for this event
2422            Cursor cursor = null;
2423            long calId;
2424            try {
2425                cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
2426                        new String[] { Events.CALENDAR_ID },
2427                        null /* selection */,
2428                        null /* selectionArgs */,
2429                        null /* sort */);
2430                if (cursor == null || !cursor.moveToFirst()) {
2431                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2432                        Log.d(TAG, "Couldn't find " + eventId + " in Events table");
2433                    }
2434                    return;
2435                }
2436                calId = cursor.getLong(0);
2437            } finally {
2438                if (cursor != null) {
2439                    cursor.close();
2440                }
2441            }
2442
2443            // Get the owner email for this Calendar
2444            String calendarEmail = null;
2445            cursor = null;
2446            try {
2447                cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2448                        new String[] { Calendars.OWNER_ACCOUNT },
2449                        null /* selection */,
2450                        null /* selectionArgs */,
2451                        null /* sort */);
2452                if (cursor == null || !cursor.moveToFirst()) {
2453                    if (Log.isLoggable(TAG, Log.DEBUG)) {
2454                        Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2455                    }
2456                    return;
2457                }
2458                calendarEmail = cursor.getString(0);
2459            } finally {
2460                if (cursor != null) {
2461                    cursor.close();
2462                }
2463            }
2464
2465            if (calendarEmail == null) {
2466                return;
2467            }
2468
2469            // Get the email address for this attendee
2470            String attendeeEmail = null;
2471            if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
2472                attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
2473            }
2474
2475            // If the attendee email does not match the calendar email, then this
2476            // attendee is not the owner of this calendar so we don't update the
2477            // selfAttendeeStatus in the event.
2478            if (!calendarEmail.equals(attendeeEmail)) {
2479                return;
2480            }
2481        }
2482
2483        int status = Attendees.ATTENDEE_STATUS_NONE;
2484        if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
2485            int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
2486            if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
2487                status = Attendees.ATTENDEE_STATUS_ACCEPTED;
2488            }
2489        }
2490
2491        if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
2492            status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
2493        }
2494
2495        ContentValues values = new ContentValues();
2496        values.put(Events.SELF_ATTENDEE_STATUS, status);
2497        db.update(Tables.EVENTS, values, SQL_WHERE_ID,
2498                new String[] {String.valueOf(eventId)});
2499    }
2500
2501    /**
2502     * Calculates the "last date" of the event.  For a regular event this is the start time
2503     * plus the duration.  For a recurring event this is the start date of the last event in
2504     * the recurrence, plus the duration.  The event recurs forever, this returns -1.  If
2505     * the recurrence rule can't be parsed, this returns -1.
2506     *
2507     * @param values
2508     * @return the date, in milliseconds, since the start of the epoch (UTC), or -1 if an
2509     *   exceptional condition exists.
2510     * @throws DateException
2511     */
2512    long calculateLastDate(ContentValues values)
2513            throws DateException {
2514        // Allow updates to some event fields like the title or hasAlarm
2515        // without requiring DTSTART.
2516        if (!values.containsKey(Events.DTSTART)) {
2517            if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
2518                    || values.containsKey(Events.DURATION)
2519                    || values.containsKey(Events.EVENT_TIMEZONE)
2520                    || values.containsKey(Events.RDATE)
2521                    || values.containsKey(Events.EXRULE)
2522                    || values.containsKey(Events.EXDATE)) {
2523                throw new RuntimeException("DTSTART field missing from event");
2524            }
2525            return -1;
2526        }
2527        long dtstartMillis = values.getAsLong(Events.DTSTART);
2528        long lastMillis = -1;
2529
2530        // Can we use dtend with a repeating event?  What does that even
2531        // mean?
2532        // NOTE: if the repeating event has a dtend, we convert it to a
2533        // duration during event processing, so this situation should not
2534        // occur.
2535        Long dtEnd = values.getAsLong(Events.DTEND);
2536        if (dtEnd != null) {
2537            lastMillis = dtEnd;
2538        } else {
2539            // find out how long it is
2540            Duration duration = new Duration();
2541            String durationStr = values.getAsString(Events.DURATION);
2542            if (durationStr != null) {
2543                duration.parse(durationStr);
2544            }
2545
2546            RecurrenceSet recur = null;
2547            try {
2548                recur = new RecurrenceSet(values);
2549            } catch (EventRecurrence.InvalidFormatException e) {
2550                if (Log.isLoggable(TAG, Log.WARN)) {
2551                    Log.w(TAG, "Could not parse RRULE recurrence string: " +
2552                            values.get(CalendarContract.Events.RRULE), e);
2553                }
2554                // TODO: this should throw an exception or return a distinct error code
2555                return lastMillis; // -1
2556            }
2557
2558            if (null != recur && recur.hasRecurrence()) {
2559                // the event is repeating, so find the last date it
2560                // could appear on
2561
2562                String tz = values.getAsString(Events.EVENT_TIMEZONE);
2563
2564                if (TextUtils.isEmpty(tz)) {
2565                    // floating timezone
2566                    tz = Time.TIMEZONE_UTC;
2567                }
2568                Time dtstartLocal = new Time(tz);
2569
2570                dtstartLocal.set(dtstartMillis);
2571
2572                RecurrenceProcessor rp = new RecurrenceProcessor();
2573                lastMillis = rp.getLastOccurence(dtstartLocal, recur);
2574                if (lastMillis == -1) {
2575                    // repeats forever
2576                    return lastMillis;  // -1
2577                }
2578            } else {
2579                // the event is not repeating, just use dtstartMillis
2580                lastMillis = dtstartMillis;
2581            }
2582
2583            // that was the beginning of the event.  this is the end.
2584            lastMillis = duration.addTo(lastMillis);
2585        }
2586        return lastMillis;
2587    }
2588
2589    /**
2590     * Add LAST_DATE to values.
2591     * @param values the ContentValues (in/out)
2592     * @return values on success, null on failure
2593     */
2594    private ContentValues updateLastDate(ContentValues values) {
2595        try {
2596            long last = calculateLastDate(values);
2597            if (last != -1) {
2598                values.put(Events.LAST_DATE, last);
2599            }
2600
2601            return values;
2602        } catch (DateException e) {
2603            // don't add it if there was an error
2604            if (Log.isLoggable(TAG, Log.WARN)) {
2605                Log.w(TAG, "Could not calculate last date.", e);
2606            }
2607            return null;
2608        }
2609    }
2610
2611    /**
2612     * Creates or updates an entry in the EventsRawTimes table.
2613     *
2614     * @param eventId The ID of the event that was just created or is being updated.
2615     * @param values For a new event, the full set of event values; for an updated event,
2616     *   the set of values that are being changed.
2617     */
2618    private void updateEventRawTimesLocked(long eventId, ContentValues values) {
2619        ContentValues rawValues = new ContentValues();
2620
2621        rawValues.put(CalendarContract.EventsRawTimes.EVENT_ID, eventId);
2622
2623        String timezone = values.getAsString(Events.EVENT_TIMEZONE);
2624
2625        boolean allDay = false;
2626        Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2627        if (allDayInteger != null) {
2628            allDay = allDayInteger != 0;
2629        }
2630
2631        if (allDay || TextUtils.isEmpty(timezone)) {
2632            // floating timezone
2633            timezone = Time.TIMEZONE_UTC;
2634        }
2635
2636        Time time = new Time(timezone);
2637        time.allDay = allDay;
2638        Long dtstartMillis = values.getAsLong(Events.DTSTART);
2639        if (dtstartMillis != null) {
2640            time.set(dtstartMillis);
2641            rawValues.put(CalendarContract.EventsRawTimes.DTSTART_2445, time.format2445());
2642        }
2643
2644        Long dtendMillis = values.getAsLong(Events.DTEND);
2645        if (dtendMillis != null) {
2646            time.set(dtendMillis);
2647            rawValues.put(CalendarContract.EventsRawTimes.DTEND_2445, time.format2445());
2648        }
2649
2650        Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
2651        if (originalInstanceMillis != null) {
2652            // This is a recurrence exception so we need to get the all-day
2653            // status of the original recurring event in order to format the
2654            // date correctly.
2655            allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
2656            if (allDayInteger != null) {
2657                time.allDay = allDayInteger != 0;
2658            }
2659            time.set(originalInstanceMillis);
2660            rawValues.put(CalendarContract.EventsRawTimes.ORIGINAL_INSTANCE_TIME_2445,
2661                    time.format2445());
2662        }
2663
2664        Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
2665        if (lastDateMillis != null) {
2666            time.allDay = allDay;
2667            time.set(lastDateMillis);
2668            rawValues.put(CalendarContract.EventsRawTimes.LAST_DATE_2445, time.format2445());
2669        }
2670
2671        mDbHelper.eventsRawTimesReplace(rawValues);
2672    }
2673
2674    @Override
2675    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
2676            boolean callerIsSyncAdapter) {
2677        if (Log.isLoggable(TAG, Log.VERBOSE)) {
2678            Log.v(TAG, "deleteInTransaction: " + uri);
2679        }
2680        final int match = sUriMatcher.match(uri);
2681        verifyTransactionAllowed(TRANSACTION_DELETE, uri, null, callerIsSyncAdapter, match,
2682                selection, selectionArgs);
2683
2684        switch (match) {
2685            case SYNCSTATE:
2686                return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
2687
2688            case SYNCSTATE_ID:
2689                String selectionWithId = (SyncState._ID + "=?")
2690                        + (selection == null ? "" : " AND (" + selection + ")");
2691                // Prepend id to selectionArgs
2692                selectionArgs = insertSelectionArg(selectionArgs,
2693                        String.valueOf(ContentUris.parseId(uri)));
2694                return mDbHelper.getSyncState().delete(mDb, selectionWithId,
2695                        selectionArgs);
2696
2697            case EVENTS:
2698            {
2699                int result = 0;
2700                selection = appendSyncAccountToSelection(uri, selection);
2701
2702                // Query this event to get the ids to delete.
2703                Cursor cursor = mDb.query(Views.EVENTS, ID_ONLY_PROJECTION,
2704                        selection, selectionArgs, null /* groupBy */,
2705                        null /* having */, null /* sortOrder */);
2706                try {
2707                    while (cursor.moveToNext()) {
2708                        long id = cursor.getLong(0);
2709                        result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
2710                    }
2711                    mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
2712                    sendUpdateNotification(callerIsSyncAdapter);
2713                } finally {
2714                    cursor.close();
2715                    cursor = null;
2716                }
2717                return result;
2718            }
2719            case EVENTS_ID:
2720            {
2721                long id = ContentUris.parseId(uri);
2722                if (selection != null) {
2723                    throw new UnsupportedOperationException("CalendarProvider2 "
2724                            + "doesn't support selection based deletion for type "
2725                            + match);
2726                }
2727                return deleteEventInternal(id, callerIsSyncAdapter, false /* isBatch */);
2728            }
2729            case EXCEPTION_ID2:
2730            {
2731                // This will throw NumberFormatException on missing or malformed input.
2732                List<String> segments = uri.getPathSegments();
2733                long eventId = Long.parseLong(segments.get(1));
2734                long excepId = Long.parseLong(segments.get(2));
2735                // TODO: verify that this is an exception instance (has an ORIGINAL_ID field
2736                //       that matches the supplied eventId)
2737                return deleteEventInternal(excepId, callerIsSyncAdapter, false /* isBatch */);
2738            }
2739            case ATTENDEES:
2740            {
2741                if (callerIsSyncAdapter) {
2742                    return mDb.delete(Tables.ATTENDEES, selection, selectionArgs);
2743                } else {
2744                    return deleteFromTable(Tables.ATTENDEES, uri, selection, selectionArgs);
2745                }
2746            }
2747            case ATTENDEES_ID:
2748            {
2749                if (selection != null) {
2750                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
2751                }
2752                if (callerIsSyncAdapter) {
2753                    long id = ContentUris.parseId(uri);
2754                    return mDb.delete(Tables.ATTENDEES, SQL_WHERE_ID,
2755                            new String[] {String.valueOf(id)});
2756                } else {
2757                    return deleteFromTable(Tables.ATTENDEES, uri, null /* selection */,
2758                                           null /* selectionArgs */);
2759                }
2760            }
2761            case REMINDERS:
2762            {
2763                if (callerIsSyncAdapter) {
2764                    return mDb.delete(Tables.REMINDERS, selection, selectionArgs);
2765                } else {
2766                    return deleteFromTable(Tables.REMINDERS, uri, selection, selectionArgs);
2767                }
2768            }
2769            case REMINDERS_ID:
2770            {
2771                if (selection != null) {
2772                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
2773                }
2774                if (callerIsSyncAdapter) {
2775                    long id = ContentUris.parseId(uri);
2776                    return mDb.delete(Tables.REMINDERS, SQL_WHERE_ID,
2777                            new String[] {String.valueOf(id)});
2778                } else {
2779                    return deleteFromTable(Tables.REMINDERS, uri, null /* selection */,
2780                                           null /* selectionArgs */);
2781                }
2782            }
2783            case EXTENDED_PROPERTIES:
2784            {
2785                if (callerIsSyncAdapter) {
2786                    return mDb.delete(Tables.EXTENDED_PROPERTIES, selection, selectionArgs);
2787                } else {
2788                    return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, selection,
2789                            selectionArgs);
2790                }
2791            }
2792            case EXTENDED_PROPERTIES_ID:
2793            {
2794                if (selection != null) {
2795                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
2796                }
2797                if (callerIsSyncAdapter) {
2798                    long id = ContentUris.parseId(uri);
2799                    return mDb.delete(Tables.EXTENDED_PROPERTIES, SQL_WHERE_ID,
2800                            new String[] {String.valueOf(id)});
2801                } else {
2802                    return deleteFromTable(Tables.EXTENDED_PROPERTIES, uri, null /* selection */,
2803                                           null /* selectionArgs */);
2804                }
2805            }
2806            case CALENDAR_ALERTS:
2807            {
2808                if (callerIsSyncAdapter) {
2809                    return mDb.delete(Tables.CALENDAR_ALERTS, selection, selectionArgs);
2810                } else {
2811                    return deleteFromTable(Tables.CALENDAR_ALERTS, uri, selection, selectionArgs);
2812                }
2813            }
2814            case CALENDAR_ALERTS_ID:
2815            {
2816                if (selection != null) {
2817                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
2818                }
2819                // Note: dirty bit is not set for Alerts because it is not synced.
2820                // It is generated from Reminders, which is synced.
2821                long id = ContentUris.parseId(uri);
2822                return mDb.delete(Tables.CALENDAR_ALERTS, SQL_WHERE_ID,
2823                        new String[] {String.valueOf(id)});
2824            }
2825            case CALENDARS_ID:
2826                StringBuilder selectionSb = new StringBuilder(Calendars._ID + "=");
2827                selectionSb.append(uri.getPathSegments().get(1));
2828                if (!TextUtils.isEmpty(selection)) {
2829                    selectionSb.append(" AND (");
2830                    selectionSb.append(selection);
2831                    selectionSb.append(')');
2832                }
2833                selection = selectionSb.toString();
2834                // fall through to CALENDARS for the actual delete
2835            case CALENDARS:
2836                selection = appendAccountToSelection(uri, selection);
2837                return deleteMatchingCalendars(selection, selectionArgs);
2838            case INSTANCES:
2839            case INSTANCES_BY_DAY:
2840            case EVENT_DAYS:
2841            case PROVIDER_PROPERTIES:
2842                throw new UnsupportedOperationException("Cannot delete that URL");
2843            default:
2844                throw new IllegalArgumentException("Unknown URL " + uri);
2845        }
2846    }
2847
2848    private int deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch) {
2849        int result = 0;
2850        String selectionArgs[] = new String[] {String.valueOf(id)};
2851
2852        // Query this event to get the fields needed for deleting.
2853        Cursor cursor = mDb.query(Tables.EVENTS, EVENTS_PROJECTION,
2854                SQL_WHERE_ID, selectionArgs,
2855                null /* groupBy */,
2856                null /* having */, null /* sortOrder */);
2857        try {
2858            if (cursor.moveToNext()) {
2859                result = 1;
2860                String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
2861                boolean emptySyncId = TextUtils.isEmpty(syncId);
2862
2863                // If this was a recurring event or a recurrence
2864                // exception, then force a recalculation of the
2865                // instances.
2866                String rrule = cursor.getString(EVENTS_RRULE_INDEX);
2867                String rdate = cursor.getString(EVENTS_RDATE_INDEX);
2868                String origId = cursor.getString(EVENTS_ORIGINAL_ID_INDEX);
2869                String origSyncId = cursor.getString(EVENTS_ORIGINAL_SYNC_ID_INDEX);
2870                if (isRecurrenceEvent(rrule, rdate, origId, origSyncId)) {
2871                    mMetaData.clearInstanceRange();
2872                }
2873                boolean isRecurrence = !TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate);
2874
2875                // we clean the Events and Attendees table if the caller is CalendarSyncAdapter
2876                // or if the event is local (no syncId)
2877                //
2878                // The EVENTS_CLEANUP_TRIGGER_SQL trigger will remove all associated data
2879                // (Attendees, Instances, Reminders, etc).
2880                if (callerIsSyncAdapter || emptySyncId) {
2881                    mDb.delete(Tables.EVENTS, SQL_WHERE_ID, selectionArgs);
2882
2883                    // If this is a recurrence, and the event was never synced with the server,
2884                    // we want to delete any exceptions as well.  (If it has been to the server,
2885                    // we'll let the sync adapter delete the events explicitly.)  We assume that,
2886                    // if the recurrence hasn't been synced, the exceptions haven't either.
2887                    if (isRecurrence && emptySyncId) {
2888                        mDb.delete(Tables.EVENTS, SQL_WHERE_ORIGINAL_ID, selectionArgs);
2889                    }
2890                } else {
2891                    // Event is on the server, so we "soft delete", i.e. mark as deleted so that
2892                    // the sync adapter has a chance to tell the server about the deletion.  After
2893                    // the server sees the change, the sync adapter will do the "hard delete"
2894                    // (above).
2895                    ContentValues values = new ContentValues();
2896                    values.put(Events.DELETED, 1);
2897                    values.put(Events.DIRTY, 1);
2898                    mDb.update(Tables.EVENTS, values, SQL_WHERE_ID, selectionArgs);
2899
2900                    // Exceptions that have been synced shouldn't be deleted -- the sync
2901                    // adapter will take care of that -- but we want to "soft delete" them so
2902                    // that they will be removed from the instances list.
2903                    // TODO: this seems to confuse the sync adapter, and leaves you with an
2904                    //       invisible "ghost" event after the server sync.  Maybe we can fix
2905                    //       this by making instance generation smarter?  Not vital, since the
2906                    //       exception instances disappear after the server sync.
2907                    //mDb.update(Tables.EVENTS, values, SQL_WHERE_ORIGINAL_ID_HAS_SYNC_ID,
2908                    //        selectionArgs);
2909
2910                    // It's possible for the original event to be on the server but have
2911                    // exceptions that aren't.  We want to remove all events with a matching
2912                    // original_id and an empty _sync_id.
2913                    mDb.delete(Tables.EVENTS, SQL_WHERE_ORIGINAL_ID_NO_SYNC_ID,
2914                            selectionArgs);
2915
2916                    // Delete associated data; attendees, however, are deleted with the actual event
2917                    //  so that the sync adapter is able to notify attendees of the cancellation.
2918                    mDb.delete(Tables.INSTANCES, SQL_WHERE_EVENT_ID, selectionArgs);
2919                    mDb.delete(Tables.EVENTS_RAW_TIMES, SQL_WHERE_EVENT_ID, selectionArgs);
2920                    mDb.delete(Tables.REMINDERS, SQL_WHERE_EVENT_ID, selectionArgs);
2921                    mDb.delete(Tables.CALENDAR_ALERTS, SQL_WHERE_EVENT_ID, selectionArgs);
2922                    mDb.delete(Tables.EXTENDED_PROPERTIES, SQL_WHERE_EVENT_ID,
2923                            selectionArgs);
2924                }
2925            }
2926        } finally {
2927            cursor.close();
2928            cursor = null;
2929        }
2930
2931        if (!isBatch) {
2932            mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
2933            sendUpdateNotification(callerIsSyncAdapter);
2934        }
2935        return result;
2936    }
2937
2938    /**
2939     * Delete rows from a table and mark corresponding events as dirty.
2940     * @param table The table to delete from
2941     * @param uri The URI specifying the rows
2942     * @param selection for the query
2943     * @param selectionArgs for the query
2944     */
2945    private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) {
2946        // Note that the query will return data according to the access restrictions,
2947        // so we don't need to worry about deleting data we don't have permission to read.
2948        final Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
2949        final ContentValues values = new ContentValues();
2950        values.put(Events.DIRTY, "1");
2951        int count = 0;
2952        try {
2953            while(c.moveToNext()) {
2954                final long id = c.getLong(ID_INDEX);
2955                final long event_id = c.getLong(EVENT_ID_INDEX);
2956                mDbHelper.duplicateEvent(event_id);
2957                mDb.delete(table, SQL_WHERE_ID, new String[]{String.valueOf(id)});
2958                mDb.update(Tables.EVENTS, values, SQL_WHERE_ID,
2959                        new String[] {String.valueOf(event_id)});
2960                count++;
2961            }
2962        } finally {
2963            c.close();
2964        }
2965        return count;
2966    }
2967
2968    /**
2969     * Update rows in a table and mark corresponding events as dirty.
2970     * @param table The table to delete from
2971     * @param values The values to update
2972     * @param uri The URI specifying the rows
2973     * @param selection for the query
2974     * @param selectionArgs for the query
2975     */
2976    private int updateInTable(String table, ContentValues values, Uri uri, String selection,
2977            String[] selectionArgs) {
2978        // Note that the query will return data according to the access restrictions,
2979        // so we don't need to worry about deleting data we don't have permission to read.
2980        final Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
2981        final ContentValues dirtyValues = new ContentValues();
2982        dirtyValues.put(Events.DIRTY, "1");
2983        int count = 0;
2984        try {
2985            while(c.moveToNext()) {
2986                final long id = c.getLong(ID_INDEX);
2987                final long event_id = c.getLong(EVENT_ID_INDEX);
2988                mDbHelper.duplicateEvent(event_id);
2989                mDb.update(table, values, SQL_WHERE_ID, new String[] {String.valueOf(id)});
2990                mDb.update(Tables.EVENTS, dirtyValues, SQL_WHERE_ID,
2991                        new String[] {String.valueOf(event_id)});
2992                count++;
2993            }
2994        } finally {
2995            c.close();
2996        }
2997        return count;
2998    }
2999
3000    private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
3001        // query to find all the calendars that match, for each
3002        // - delete calendar subscription
3003        // - delete calendar
3004        Cursor c = mDb.query(Tables.CALENDARS, sCalendarsIdProjection, selection,
3005                selectionArgs,
3006                null /* groupBy */,
3007                null /* having */,
3008                null /* sortOrder */);
3009        if (c == null) {
3010            return 0;
3011        }
3012        try {
3013            while (c.moveToNext()) {
3014                long id = c.getLong(CALENDARS_INDEX_ID);
3015                modifyCalendarSubscription(id, false /* not selected */);
3016            }
3017        } finally {
3018            c.close();
3019        }
3020        return mDb.delete(Tables.CALENDARS, selection, selectionArgs);
3021    }
3022
3023    private boolean doesEventExistForSyncId(String syncId) {
3024        if (syncId == null) {
3025            if (Log.isLoggable(TAG, Log.WARN)) {
3026                Log.w(TAG, "SyncID cannot be null: " + syncId);
3027            }
3028            return false;
3029        }
3030        long count = DatabaseUtils.longForQuery(mDb, SQL_SELECT_COUNT_FOR_SYNC_ID,
3031                new String[] { syncId });
3032        return (count > 0);
3033    }
3034
3035    // Check if an UPDATE with STATUS_CANCEL means that we will need to do an Update (instead of
3036    // a Deletion)
3037    //
3038    // Deletion will be done only and only if:
3039    // - event status = canceled
3040    // - event is a recurrence exception that does not have its original (parent) event anymore
3041    //
3042    // This is due to the Server semantics that generate STATUS_CANCELED for both creation
3043    // and deletion of a recurrence exception
3044    // See bug #3218104
3045    private boolean doesStatusCancelUpdateMeanUpdate(ContentValues values,
3046            ContentValues modValues) {
3047        boolean isStatusCanceled = modValues.containsKey(Events.STATUS) &&
3048                (modValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED);
3049        if (isStatusCanceled) {
3050            String originalSyncId = values.getAsString(Events.ORIGINAL_SYNC_ID);
3051
3052            if (!TextUtils.isEmpty(originalSyncId)) {
3053                // This event is an exception.  See if the recurring event still exists.
3054                return doesEventExistForSyncId(originalSyncId);
3055            }
3056        }
3057        // This is the normal case, we just want an UPDATE
3058        return true;
3059    }
3060
3061
3062    /**
3063     * Handles a request to update one or more events.
3064     * <p>
3065     * The original event(s) will be loaded from the database, merged with the new values,
3066     * and the result checked for validity.  In some cases this will alter the supplied
3067     * arguments (e.g. zeroing out the times on all-day events), change additional fields (e.g.
3068     * update LAST_DATE when DTSTART changes), or cause modifications to other tables (e.g. reset
3069     * Instances when a recurrence rule changes).
3070     *
3071     * @param cursor The set of events to update.
3072     * @param updateValues The changes to apply to each event.
3073     * @param callerIsSyncAdapter Indicates if the request comes from the sync adapter.
3074     * @return the number of rows updated
3075     */
3076    private int handleUpdateEvents(Cursor cursor, ContentValues updateValues,
3077            boolean callerIsSyncAdapter) {
3078        /*
3079         * For a single event, we can just load the event, merge modValues in, perform any
3080         * fix-ups (putting changes into modValues), check validity, and then update().  We have
3081         * to be careful that our fix-ups don't confuse the sync adapter.
3082         *
3083         * For multiple events, we need to load, merge, and validate each event individually.
3084         * If no single-event-specific changes need to be made, we could just issue the original
3085         * bulk update, which would be more efficient than a series of individual updates.
3086         * However, doing so would prevent us from taking advantage of the partial-update
3087         * mechanism.
3088         */
3089        if (cursor.getCount() > 1) {
3090            if (Log.isLoggable(TAG, Log.DEBUG)) {
3091                Log.d(TAG, "Performing update on " + cursor.getCount() + " events");
3092            }
3093        }
3094        while (cursor.moveToNext()) {
3095            // Load the event into a ContentValues object, and merge the modifications in.
3096            ContentValues modValues = new ContentValues(updateValues);
3097            ContentValues values = new ContentValues();
3098            DatabaseUtils.cursorRowToContentValues(cursor, values);
3099            values.putAll(modValues);
3100
3101            // Validate the combined event.
3102            if (callerIsSyncAdapter) {
3103                scrubEventData(values, modValues);
3104            } else {
3105                validateEventData(values);
3106            }
3107
3108            // Look for any updates that could affect LAST_DATE.  It's defined as the end of
3109            // the last meeting, so we need to pay attention to DURATION.
3110            if (modValues.containsKey(Events.DTSTART) ||
3111                    modValues.containsKey(Events.DTEND) ||
3112                    modValues.containsKey(Events.DURATION) ||
3113                    modValues.containsKey(Events.EVENT_TIMEZONE) ||
3114                    modValues.containsKey(Events.RRULE) ||
3115                    modValues.containsKey(Events.RDATE) ||
3116                    modValues.containsKey(Events.EXRULE) ||
3117                    modValues.containsKey(Events.EXDATE)) {
3118                long newLastDate;
3119                try {
3120                    newLastDate = calculateLastDate(values);
3121                } catch (DateException de) {
3122                    throw new IllegalArgumentException("Unable to compute LAST_DATE", de);
3123                }
3124                Long oldLastDateObj = values.getAsLong(Events.LAST_DATE);
3125                long oldLastDate = (oldLastDateObj == null) ? -1 : oldLastDateObj;
3126                if (oldLastDate != newLastDate) {
3127                    // This overwrites any caller-supplied LAST_DATE.  This is okay, because the
3128                    // caller isn't supposed to be messing with the LAST_DATE field.
3129                    if (newLastDate < 0) {
3130                        modValues.putNull(Events.LAST_DATE);
3131                    } else {
3132                        modValues.put(Events.LAST_DATE, newLastDate);
3133                    }
3134                }
3135            }
3136
3137            if (!callerIsSyncAdapter) {
3138                modValues.put(Events.DIRTY, 1);
3139            }
3140
3141            // Disallow updating the attendee status in the Events
3142            // table.  In the future, we could support this but we
3143            // would have to query and update the attendees table
3144            // to keep the values consistent.
3145            if (modValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
3146                throw new IllegalArgumentException("Updating "
3147                        + Events.SELF_ATTENDEE_STATUS
3148                        + " in Events table is not allowed.");
3149            }
3150
3151            if (fixAllDayTime(values, modValues)) {
3152                if (Log.isLoggable(TAG, Log.WARN)) {
3153                    Log.w(TAG, "handleUpdateEvents: " +
3154                            "allDay is true but sec, min, hour were not 0.");
3155                }
3156            }
3157
3158            // For taking care about recurrences exceptions cancelations, check if this needs
3159            //  to be an UPDATE or a DELETE
3160            boolean isUpdate = doesStatusCancelUpdateMeanUpdate(values, modValues);
3161
3162            long id = values.getAsLong(Events._ID);
3163
3164            if (isUpdate) {
3165                // If a user made a change, possibly duplicate the event so we can do a partial
3166                // update. If a sync adapter made a change and that change marks an event as
3167                // un-dirty, remove any duplicates that may have been created earlier.
3168                if (!callerIsSyncAdapter) {
3169                    mDbHelper.duplicateEvent(id);
3170                } else {
3171                    if (modValues.containsKey(Events.DIRTY)
3172                            && modValues.getAsInteger(Events.DIRTY) == 0) {
3173                        mDbHelper.removeDuplicateEvent(id);
3174                    }
3175                }
3176                int result = mDb.update(Tables.EVENTS, modValues, SQL_WHERE_ID,
3177                        new String[] { String.valueOf(id) });
3178                if (result > 0) {
3179                    updateEventRawTimesLocked(id, modValues);
3180                    mInstancesHelper.updateInstancesLocked(modValues, id,
3181                            false /* not a new event */, mDb);
3182
3183                    // XXX: should we also be doing this when RRULE changes (e.g. instances
3184                    //      are introduced or removed?)
3185                    if (modValues.containsKey(Events.DTSTART) ||
3186                            modValues.containsKey(Events.STATUS)) {
3187                        // If this is a cancellation knock it out
3188                        // of the instances table
3189                        if (modValues.containsKey(Events.STATUS) &&
3190                                modValues.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED) {
3191                            String[] args = new String[] {String.valueOf(id)};
3192                            mDb.delete(Tables.INSTANCES, SQL_WHERE_EVENT_ID, args);
3193                        }
3194
3195                        // The start time or status of the event changed, so run the
3196                        // event alarm scheduler.
3197                        if (Log.isLoggable(TAG, Log.DEBUG)) {
3198                            Log.d(TAG, "updateInternal() changing event");
3199                        }
3200                        mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
3201                    }
3202
3203                    sendUpdateNotification(id, callerIsSyncAdapter);
3204                }
3205            } else {
3206                deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
3207                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
3208                sendUpdateNotification(callerIsSyncAdapter);
3209            }
3210        }
3211
3212        return cursor.getCount();
3213    }
3214
3215    @Override
3216    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
3217            String[] selectionArgs, boolean callerIsSyncAdapter) {
3218        if (Log.isLoggable(TAG, Log.VERBOSE)) {
3219            Log.v(TAG, "updateInTransaction: " + uri);
3220        }
3221        final int match = sUriMatcher.match(uri);
3222        verifyTransactionAllowed(TRANSACTION_UPDATE, uri, values, callerIsSyncAdapter, match,
3223                selection, selectionArgs);
3224
3225        int count = 0;
3226
3227        // TODO: remove this restriction
3228        if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS
3229                && match != EVENTS && match != CALENDARS && match != PROVIDER_PROPERTIES) {
3230            throw new IllegalArgumentException("WHERE based updates not supported");
3231        }
3232
3233        switch (match) {
3234            case SYNCSTATE:
3235                return mDbHelper.getSyncState().update(mDb, values,
3236                        appendAccountToSelection(uri, selection), selectionArgs);
3237
3238            case SYNCSTATE_ID: {
3239                selection = appendAccountToSelection(uri, selection);
3240                String selectionWithId = (SyncState._ID + "=?")
3241                        + (selection == null ? "" : " AND (" + selection + ")");
3242                // Prepend id to selectionArgs
3243                selectionArgs = insertSelectionArg(selectionArgs,
3244                        String.valueOf(ContentUris.parseId(uri)));
3245                return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs);
3246            }
3247
3248            case CALENDARS:
3249            case CALENDARS_ID:
3250            {
3251                long id;
3252                if (match == CALENDARS_ID) {
3253                    if (selection != null) {
3254                        throw new UnsupportedOperationException("Selection not permitted for "
3255                                + uri);
3256                    }
3257                    id = ContentUris.parseId(uri);
3258                } else {
3259                    // TODO: for supporting other sync adapters, we will need to
3260                    // be able to deal with the following cases:
3261                    // 1) selection to "_id=?" and pass in a selectionArgs
3262                    // 2) selection to "_id IN (1, 2, 3)"
3263                    // 3) selection to "delete=0 AND _id=1"
3264                    if (selection != null && TextUtils.equals(selection,"_id=?")) {
3265                        id = Long.parseLong(selectionArgs[0]);
3266                    } else if (selection != null && selection.startsWith("_id=")) {
3267                        // The ContentProviderOperation generates an _id=n string instead of
3268                        // adding the id to the URL, so parse that out here.
3269                        id = Long.parseLong(selection.substring(4));
3270                    } else {
3271                        return mDb.update(Tables.CALENDARS, values, selection, selectionArgs);
3272                    }
3273                }
3274                if (!callerIsSyncAdapter) {
3275                    values.put(Calendars.DIRTY, 1);
3276                }
3277                Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
3278                if (syncEvents != null) {
3279                    modifyCalendarSubscription(id, syncEvents == 1);
3280                }
3281
3282                int result = mDb.update(Tables.CALENDARS, values, SQL_WHERE_ID,
3283                        new String[] {String.valueOf(id)});
3284
3285                if (result > 0) {
3286                    // if visibility was toggled, we need to update alarms
3287                    if (values.containsKey(Calendars.VISIBLE)) {
3288                        // pass false for removeAlarms since the call to
3289                        // scheduleNextAlarmLocked will remove any alarms for
3290                        // non-visible events anyways. removeScheduledAlarmsLocked
3291                        // does not actually have the effect we want
3292                        mCalendarAlarm.scheduleNextAlarm(false);
3293                    }
3294                    // update the widget
3295                    sendUpdateNotification(callerIsSyncAdapter);
3296                }
3297
3298                return result;
3299            }
3300            case EVENTS:
3301            case EVENTS_ID:
3302            {
3303                Cursor events = null;
3304
3305                // Grab the full set of columns for each selected event.
3306                // TODO: define a projection with just the data we need (e.g. we don't need to
3307                //       validate the SYNC_* columns)
3308
3309                try {
3310                    if (match == EVENTS_ID) {
3311                        // Single event, identified by ID.
3312                        long id = ContentUris.parseId(uri);
3313                        events = mDb.query(Tables.EVENTS, null /* columns */,
3314                                SQL_WHERE_ID, new String[] { String.valueOf(id) },
3315                                null /* groupBy */, null /* having */, null /* sortOrder */);
3316                    } else {
3317                        // One or more events, identified by the selection / selectionArgs.
3318                        if (!callerIsSyncAdapter) {
3319                            // Only the sync adapter can use this URI.
3320                            throw new IllegalArgumentException("Invalid URI: " + uri);
3321                        }
3322
3323                        events = mDb.query(Tables.EVENTS, null /* columns */,
3324                                selection, selectionArgs,
3325                                null /* groupBy */, null /* having */, null /* sortOrder */);
3326                    }
3327
3328                    if (events.getCount() == 0) {
3329                        Log.w(TAG, "No events to update: uri=" + uri + " selection=" + selection +
3330                                " selectionArgs=" + Arrays.toString(selectionArgs));
3331                        return 0;
3332                    }
3333
3334                    return handleUpdateEvents(events, values, callerIsSyncAdapter);
3335                } finally {
3336                    if (events != null) {
3337                        events.close();
3338                    }
3339                }
3340            }
3341            case ATTENDEES_ID: {
3342                if (selection != null) {
3343                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
3344                }
3345                // Copy the attendee status value to the Events table.
3346                updateEventAttendeeStatus(mDb, values);
3347
3348                if (callerIsSyncAdapter) {
3349                    long id = ContentUris.parseId(uri);
3350                    return mDb.update(Tables.ATTENDEES, values, SQL_WHERE_ID,
3351                            new String[] {String.valueOf(id)});
3352                } else {
3353                    return updateInTable(Tables.ATTENDEES, values, uri, null /* selection */,
3354                            null /* selectionArgs */);
3355                }
3356            }
3357            case CALENDAR_ALERTS_ID: {
3358                if (selection != null) {
3359                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
3360                }
3361                // Note: dirty bit is not set for Alerts because it is not synced.
3362                // It is generated from Reminders, which is synced.
3363                long id = ContentUris.parseId(uri);
3364                return mDb.update(Tables.CALENDAR_ALERTS, values, SQL_WHERE_ID,
3365                        new String[] {String.valueOf(id)});
3366            }
3367            case CALENDAR_ALERTS: {
3368                // Note: dirty bit is not set for Alerts because it is not synced.
3369                // It is generated from Reminders, which is synced.
3370                return mDb.update(Tables.CALENDAR_ALERTS, values, selection, selectionArgs);
3371            }
3372            case REMINDERS_ID: {
3373                if (selection != null) {
3374                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
3375                }
3376                if (callerIsSyncAdapter) {
3377                    long id = ContentUris.parseId(uri);
3378                    count = mDb.update(Tables.REMINDERS, values, SQL_WHERE_ID,
3379                            new String[] {String.valueOf(id)});
3380                } else {
3381                    count = updateInTable(Tables.REMINDERS, values, uri, null /* selection */,
3382                            null /* selectionArgs */);
3383                }
3384
3385                // Reschedule the event alarms because the
3386                // "minutes" field may have changed.
3387                if (Log.isLoggable(TAG, Log.DEBUG)) {
3388                    Log.d(TAG, "updateInternal() changing reminder");
3389                }
3390                mCalendarAlarm.scheduleNextAlarm(false /* do not remove alarms */);
3391                return count;
3392            }
3393            case EXTENDED_PROPERTIES_ID: {
3394                if (selection != null) {
3395                    throw new UnsupportedOperationException("Selection not permitted for " + uri);
3396                }
3397                if (callerIsSyncAdapter) {
3398                    long id = ContentUris.parseId(uri);
3399                    return mDb.update(Tables.EXTENDED_PROPERTIES, values, SQL_WHERE_ID,
3400                            new String[] {String.valueOf(id)});
3401                } else {
3402                    return updateInTable(Tables.EXTENDED_PROPERTIES, values, uri,
3403                            null /* selection */, null /* selectionArgs */);
3404                }
3405            }
3406            // TODO: replace the SCHEDULE_ALARM private URIs with a
3407            // service
3408            case SCHEDULE_ALARM: {
3409                mCalendarAlarm.scheduleNextAlarm(false);
3410                return 0;
3411            }
3412            case SCHEDULE_ALARM_REMOVE: {
3413                mCalendarAlarm.scheduleNextAlarm(true);
3414                return 0;
3415            }
3416
3417            case PROVIDER_PROPERTIES: {
3418                if (selection == null) {
3419                    throw new UnsupportedOperationException("Selection cannot be null for " + uri);
3420                }
3421                if (!selection.equals("key=?")) {
3422                    throw new UnsupportedOperationException("Selection should be key=? for " + uri);
3423                }
3424
3425                List<String> list = Arrays.asList(selectionArgs);
3426
3427                if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) {
3428                    throw new UnsupportedOperationException("Invalid selection key: " +
3429                            CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + " for " + uri);
3430                }
3431
3432                // Before it may be changed, save current Instances timezone for later use
3433                String timezoneInstancesBeforeUpdate = mCalendarCache.readTimezoneInstances();
3434
3435                // Update the database with the provided values (this call may change the value
3436                // of timezone Instances)
3437                int result = mDb.update(Tables.CALENDAR_CACHE, values, selection, selectionArgs);
3438
3439                // if successful, do some house cleaning:
3440                // if the timezone type is set to "home", set the Instances
3441                // timezone to the previous
3442                // if the timezone type is set to "auto", set the Instances
3443                // timezone to the current
3444                // device one
3445                // if the timezone Instances is set AND if we are in "home"
3446                // timezone type, then save the timezone Instance into
3447                // "previous" too
3448                if (result > 0) {
3449                    // If we are changing timezone type...
3450                    if (list.contains(CalendarCache.KEY_TIMEZONE_TYPE)) {
3451                        String value = values.getAsString(CalendarCache.COLUMN_NAME_VALUE);
3452                        if (value != null) {
3453                            // if we are setting timezone type to "home"
3454                            if (value.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
3455                                String previousTimezone =
3456                                        mCalendarCache.readTimezoneInstancesPrevious();
3457                                if (previousTimezone != null) {
3458                                    mCalendarCache.writeTimezoneInstances(previousTimezone);
3459                                }
3460                                // Regenerate Instances if the "home" timezone has changed
3461                                // and notify widgets
3462                                if (!timezoneInstancesBeforeUpdate.equals(previousTimezone) ) {
3463                                    regenerateInstancesTable();
3464                                    sendUpdateNotification(callerIsSyncAdapter);
3465                                }
3466                            }
3467                            // if we are setting timezone type to "auto"
3468                            else if (value.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
3469                                String localTimezone = TimeZone.getDefault().getID();
3470                                mCalendarCache.writeTimezoneInstances(localTimezone);
3471                                if (!timezoneInstancesBeforeUpdate.equals(localTimezone)) {
3472                                    regenerateInstancesTable();
3473                                    sendUpdateNotification(callerIsSyncAdapter);
3474                                }
3475                            }
3476                        }
3477                    }
3478                    // If we are changing timezone Instances...
3479                    else if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES)) {
3480                        // if we are in "home" timezone type...
3481                        if (isHomeTimezone()) {
3482                            String timezoneInstances = mCalendarCache.readTimezoneInstances();
3483                            // Update the previous value
3484                            mCalendarCache.writeTimezoneInstancesPrevious(timezoneInstances);
3485                            // Recompute Instances if the "home" timezone has changed
3486                            // and send notifications to any widgets
3487                            if (timezoneInstancesBeforeUpdate != null &&
3488                                    !timezoneInstancesBeforeUpdate.equals(timezoneInstances)) {
3489                                regenerateInstancesTable();
3490                                sendUpdateNotification(callerIsSyncAdapter);
3491                            }
3492                        }
3493                    }
3494                }
3495                return result;
3496            }
3497
3498            default:
3499                throw new IllegalArgumentException("Unknown URL " + uri);
3500        }
3501    }
3502
3503    private String appendAccountFromParameterToSelection(String selection, Uri uri) {
3504        final String accountName = QueryParameterUtils.getQueryParameter(uri,
3505                CalendarContract.EventsEntity.ACCOUNT_NAME);
3506        final String accountType = QueryParameterUtils.getQueryParameter(uri,
3507                CalendarContract.EventsEntity.ACCOUNT_TYPE);
3508        if (!TextUtils.isEmpty(accountName)) {
3509            final StringBuilder sb = new StringBuilder();
3510            sb.append(Calendars.ACCOUNT_NAME + "=")
3511                    .append(DatabaseUtils.sqlEscapeString(accountName))
3512                    .append(" AND ")
3513                    .append(Calendars.ACCOUNT_TYPE)
3514                    .append(" = ")
3515                    .append(DatabaseUtils.sqlEscapeString(accountType));
3516            return appendSelection(sb, selection);
3517        } else {
3518            return selection;
3519        }
3520    }
3521
3522    private String appendLastSyncedColumnToSelection(String selection, Uri uri) {
3523        if (getIsCallerSyncAdapter(uri)) {
3524            return selection;
3525        }
3526        final StringBuilder sb = new StringBuilder();
3527        sb.append(CalendarContract.Events.LAST_SYNCED).append(" = 0");
3528        return appendSelection(sb, selection);
3529    }
3530
3531    private String appendAccountToSelection(Uri uri, String selection) {
3532        final String accountName = QueryParameterUtils.getQueryParameter(uri,
3533                CalendarContract.EventsEntity.ACCOUNT_NAME);
3534        final String accountType = QueryParameterUtils.getQueryParameter(uri,
3535                CalendarContract.EventsEntity.ACCOUNT_TYPE);
3536        if (!TextUtils.isEmpty(accountName)) {
3537            StringBuilder selectionSb = new StringBuilder(CalendarContract.Calendars.ACCOUNT_NAME
3538                    + "=" + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3539                    + CalendarContract.Calendars.ACCOUNT_TYPE + "="
3540                    + DatabaseUtils.sqlEscapeString(accountType));
3541            return appendSelection(selectionSb, selection);
3542        } else {
3543            return selection;
3544        }
3545    }
3546
3547    private String appendSyncAccountToSelection(Uri uri, String selection) {
3548        final String accountName = QueryParameterUtils.getQueryParameter(uri,
3549                CalendarContract.EventsEntity.ACCOUNT_NAME);
3550        final String accountType = QueryParameterUtils.getQueryParameter(uri,
3551                CalendarContract.EventsEntity.ACCOUNT_TYPE);
3552        if (!TextUtils.isEmpty(accountName)) {
3553            StringBuilder selectionSb = new StringBuilder(CalendarContract.Events.ACCOUNT_NAME + "="
3554                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3555                    + CalendarContract.Events.ACCOUNT_TYPE + "="
3556                    + DatabaseUtils.sqlEscapeString(accountType));
3557            return appendSelection(selectionSb, selection);
3558        } else {
3559            return selection;
3560        }
3561    }
3562
3563    private String appendSelection(StringBuilder sb, String selection) {
3564        if (!TextUtils.isEmpty(selection)) {
3565            sb.append(" AND (");
3566            sb.append(selection);
3567            sb.append(')');
3568        }
3569        return sb.toString();
3570    }
3571
3572    /**
3573     * Verifies that the operation is allowed and throws an exception if it
3574     * isn't. This defines the limits of a sync adapter call vs an app call.
3575     *
3576     * @param type The type of call, {@link #TRANSACTION_QUERY},
3577     *            {@link #TRANSACTION_INSERT}, {@link #TRANSACTION_UPDATE}, or
3578     *            {@link #TRANSACTION_DELETE}
3579     * @param uri
3580     * @param values
3581     * @param isSyncAdapter
3582     */
3583    private void verifyTransactionAllowed(int type, Uri uri, ContentValues values,
3584            boolean isSyncAdapter, int uriMatch, String selection, String[] selectionArgs) {
3585        switch (type) {
3586            case TRANSACTION_QUERY:
3587                return;
3588            case TRANSACTION_INSERT:
3589                if (uriMatch == INSTANCES) {
3590                    throw new UnsupportedOperationException(
3591                            "Inserting into instances not supported");
3592                }
3593                // Check there are no columns restricted to the provider
3594                verifyColumns(values, uriMatch);
3595                if (isSyncAdapter) {
3596                    // check that account and account type are specified
3597                    verifyHasAccount(uri, selection, selectionArgs);
3598                } else {
3599                    // check that sync only columns aren't included
3600                    verifyNoSyncColumns(values, uriMatch);
3601                }
3602                return;
3603            case TRANSACTION_UPDATE:
3604                if (uriMatch == INSTANCES) {
3605                    throw new UnsupportedOperationException("Updating instances not supported");
3606                }
3607                // Check there are no columns restricted to the provider
3608                verifyColumns(values, uriMatch);
3609                if (isSyncAdapter) {
3610                    // check that account and account type are specified
3611                    verifyHasAccount(uri, selection, selectionArgs);
3612                } else {
3613                    // check that sync only columns aren't included
3614                    verifyNoSyncColumns(values, uriMatch);
3615                }
3616                return;
3617            case TRANSACTION_DELETE:
3618                if (uriMatch == INSTANCES) {
3619                    throw new UnsupportedOperationException("Deleting instances not supported");
3620                }
3621                if (isSyncAdapter) {
3622                    // check that account and account type are specified
3623                    verifyHasAccount(uri, selection, selectionArgs);
3624                }
3625                return;
3626        }
3627    }
3628
3629    private void verifyHasAccount(Uri uri, String selection, String[] selectionArgs) {
3630        String accountName = QueryParameterUtils.getQueryParameter(uri, Calendars.ACCOUNT_NAME);
3631        String accountType = QueryParameterUtils.getQueryParameter(uri,
3632                Calendars.ACCOUNT_TYPE);
3633        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
3634            if (selection != null && selection.startsWith(ACCOUNT_SELECTION_PREFIX)) {
3635                accountName = selectionArgs[0];
3636                accountType = selectionArgs[1];
3637            }
3638        }
3639        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
3640            throw new IllegalArgumentException(
3641                    "Sync adapters must specify an account and account type: " + uri);
3642        }
3643    }
3644
3645    private void verifyColumns(ContentValues values, int uriMatch) {
3646        if (values == null || values.size() == 0) {
3647            return;
3648        }
3649        String[] columns;
3650        switch (uriMatch) {
3651            case EVENTS:
3652            case EVENTS_ID:
3653            case EVENT_ENTITIES:
3654            case EVENT_ENTITIES_ID:
3655                columns = Events.PROVIDER_WRITABLE_COLUMNS;
3656                break;
3657            default:
3658                columns = PROVIDER_WRITABLE_DEFAULT_COLUMNS;
3659                break;
3660        }
3661
3662        for (int i = 0; i < columns.length; i++) {
3663            if (values.containsKey(columns[i])) {
3664                throw new IllegalArgumentException("Only the provider may write to " + columns[i]);
3665            }
3666        }
3667    }
3668
3669    private void verifyNoSyncColumns(ContentValues values, int uriMatch) {
3670        if (values == null || values.size() == 0) {
3671            return;
3672        }
3673        String[] syncColumns;
3674        switch (uriMatch) {
3675            case CALENDARS:
3676            case CALENDARS_ID:
3677            case CALENDAR_ENTITIES:
3678            case CALENDAR_ENTITIES_ID:
3679                syncColumns = Calendars.SYNC_WRITABLE_COLUMNS;
3680                break;
3681            case EVENTS:
3682            case EVENTS_ID:
3683            case EVENT_ENTITIES:
3684            case EVENT_ENTITIES_ID:
3685                syncColumns = Events.SYNC_WRITABLE_COLUMNS;
3686                break;
3687            default:
3688                syncColumns = SYNC_WRITABLE_DEFAULT_COLUMNS;
3689                break;
3690
3691        }
3692        for (int i = 0; i < syncColumns.length; i++) {
3693            if (values.containsKey(syncColumns[i])) {
3694                throw new IllegalArgumentException("Only sync adapters may write to "
3695                        + syncColumns[i]);
3696            }
3697        }
3698    }
3699
3700    private void modifyCalendarSubscription(long id, boolean syncEvents) {
3701        // get the account, url, and current selected state
3702        // for this calendar.
3703        Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
3704                new String[] {Calendars.ACCOUNT_NAME, Calendars.ACCOUNT_TYPE,
3705                        Calendars.CAL_SYNC1, Calendars.SYNC_EVENTS},
3706                null /* selection */,
3707                null /* selectionArgs */,
3708                null /* sort */);
3709
3710        Account account = null;
3711        String calendarUrl = null;
3712        boolean oldSyncEvents = false;
3713        if (cursor != null) {
3714            try {
3715                if (cursor.moveToFirst()) {
3716                    final String accountName = cursor.getString(0);
3717                    final String accountType = cursor.getString(1);
3718                    account = new Account(accountName, accountType);
3719                    calendarUrl = cursor.getString(2);
3720                    oldSyncEvents = (cursor.getInt(3) != 0);
3721                }
3722            } finally {
3723                cursor.close();
3724            }
3725        }
3726
3727        if (account == null) {
3728            // should not happen?
3729            if (Log.isLoggable(TAG, Log.WARN)) {
3730                Log.w(TAG, "Cannot update subscription because account "
3731                        + "is empty -- should not happen.");
3732            }
3733            return;
3734        }
3735
3736        if (TextUtils.isEmpty(calendarUrl)) {
3737            // Passing in a null Url will cause it to not add any extras
3738            // Should only happen for non-google calendars.
3739            calendarUrl = null;
3740        }
3741
3742        if (oldSyncEvents == syncEvents) {
3743            // nothing to do
3744            return;
3745        }
3746
3747        // If the calendar is not selected for syncing, then don't download
3748        // events.
3749        mDbHelper.scheduleSync(account, !syncEvents, calendarUrl);
3750    }
3751
3752    /**
3753     * Call this to trigger a broadcast of the ACTION_PROVIDER_CHANGED intent.
3754     * This also provides a timeout, so any calls to this method will be batched
3755     * over a period of BROADCAST_TIMEOUT_MILLIS defined in this class.
3756     *
3757     * @param callerIsSyncAdapter whether or not the update is being triggered by a sync
3758     */
3759    private void sendUpdateNotification(boolean callerIsSyncAdapter) {
3760        // We use -1 to represent an update to all events
3761        sendUpdateNotification(-1, callerIsSyncAdapter);
3762    }
3763
3764    /**
3765     * Call this to trigger a broadcast of the ACTION_PROVIDER_CHANGED intent.
3766     * This also provides a timeout, so any calls to this method will be batched
3767     * over a period of BROADCAST_TIMEOUT_MILLIS defined in this class.  The
3768     * actual sending of the intent is done in
3769     * {@link #doSendUpdateNotification()}.
3770     *
3771     * TODO add support for eventId
3772     *
3773     * @param eventId the ID of the event that changed, or -1 for no specific event
3774     * @param callerIsSyncAdapter whether or not the update is being triggered by a sync
3775     */
3776    private void sendUpdateNotification(long eventId,
3777            boolean callerIsSyncAdapter) {
3778        // Are there any pending broadcast requests?
3779        if (mBroadcastHandler.hasMessages(UPDATE_BROADCAST_MSG)) {
3780            // Delete any pending requests, before requeuing a fresh one
3781            mBroadcastHandler.removeMessages(UPDATE_BROADCAST_MSG);
3782        } else {
3783            // Because the handler does not guarantee message delivery in
3784            // the case that the provider is killed, we need to make sure
3785            // that the provider stays alive long enough to deliver the
3786            // notification. This empty service is sufficient to "wedge" the
3787            // process until we stop it here.
3788            mContext.startService(new Intent(mContext, EmptyService.class));
3789        }
3790        // We use a much longer delay for sync-related updates, to prevent any
3791        // receivers from slowing down the sync
3792        long delay = callerIsSyncAdapter ?
3793                SYNC_UPDATE_BROADCAST_TIMEOUT_MILLIS :
3794                UPDATE_BROADCAST_TIMEOUT_MILLIS;
3795        // Despite the fact that we actually only ever use one message at a time
3796        // for now, it is really important to call obtainMessage() to get a
3797        // clean instance.  This avoids potentially infinite loops resulting
3798        // adding the same instance to the message queue twice, since the
3799        // message queue implements its linked list using a field from Message.
3800        Message msg = mBroadcastHandler.obtainMessage(UPDATE_BROADCAST_MSG);
3801        mBroadcastHandler.sendMessageDelayed(msg, delay);
3802    }
3803
3804    /**
3805     * This method should not ever be called directly, to prevent sending too
3806     * many potentially expensive broadcasts.  Instead, call
3807     * {@link #sendUpdateNotification(boolean)} instead.
3808     *
3809     * @see #sendUpdateNotification(boolean)
3810     */
3811    private void doSendUpdateNotification() {
3812        Intent intent = new Intent(Intent.ACTION_PROVIDER_CHANGED,
3813                CalendarContract.CONTENT_URI);
3814        if (Log.isLoggable(TAG, Log.INFO)) {
3815            Log.i(TAG, "Sending notification intent: " + intent);
3816        }
3817        mContext.sendBroadcast(intent, null);
3818    }
3819
3820    private static final int TRANSACTION_QUERY = 0;
3821    private static final int TRANSACTION_INSERT = 1;
3822    private static final int TRANSACTION_UPDATE = 2;
3823    private static final int TRANSACTION_DELETE = 3;
3824
3825    // @formatter:off
3826    private static final String[] SYNC_WRITABLE_DEFAULT_COLUMNS = new String[] {
3827        CalendarContract.Calendars.DIRTY,
3828        CalendarContract.Calendars._SYNC_ID
3829    };
3830    private static final String[] PROVIDER_WRITABLE_DEFAULT_COLUMNS = new String[] {
3831    };
3832    // @formatter:on
3833
3834    private static final int EVENTS = 1;
3835    private static final int EVENTS_ID = 2;
3836    private static final int INSTANCES = 3;
3837    private static final int CALENDARS = 4;
3838    private static final int CALENDARS_ID = 5;
3839    private static final int ATTENDEES = 6;
3840    private static final int ATTENDEES_ID = 7;
3841    private static final int REMINDERS = 8;
3842    private static final int REMINDERS_ID = 9;
3843    private static final int EXTENDED_PROPERTIES = 10;
3844    private static final int EXTENDED_PROPERTIES_ID = 11;
3845    private static final int CALENDAR_ALERTS = 12;
3846    private static final int CALENDAR_ALERTS_ID = 13;
3847    private static final int CALENDAR_ALERTS_BY_INSTANCE = 14;
3848    private static final int INSTANCES_BY_DAY = 15;
3849    private static final int SYNCSTATE = 16;
3850    private static final int SYNCSTATE_ID = 17;
3851    private static final int EVENT_ENTITIES = 18;
3852    private static final int EVENT_ENTITIES_ID = 19;
3853    private static final int EVENT_DAYS = 20;
3854    private static final int SCHEDULE_ALARM = 21;
3855    private static final int SCHEDULE_ALARM_REMOVE = 22;
3856    private static final int TIME = 23;
3857    private static final int CALENDAR_ENTITIES = 24;
3858    private static final int CALENDAR_ENTITIES_ID = 25;
3859    private static final int INSTANCES_SEARCH = 26;
3860    private static final int INSTANCES_SEARCH_BY_DAY = 27;
3861    private static final int PROVIDER_PROPERTIES = 28;
3862    private static final int EXCEPTION_ID = 29;
3863    private static final int EXCEPTION_ID2 = 30;
3864
3865    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
3866    private static final HashMap<String, String> sInstancesProjectionMap;
3867    protected static final HashMap<String, String> sEventsProjectionMap;
3868    private static final HashMap<String, String> sEventEntitiesProjectionMap;
3869    private static final HashMap<String, String> sAttendeesProjectionMap;
3870    private static final HashMap<String, String> sRemindersProjectionMap;
3871    private static final HashMap<String, String> sCalendarAlertsProjectionMap;
3872    private static final HashMap<String, String> sCalendarCacheProjectionMap;
3873    private static final HashMap<String, String> sCountProjectionMap;
3874
3875    static {
3876        sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/when/*/*", INSTANCES);
3877        sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY);
3878        sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/search/*/*/*", INSTANCES_SEARCH);
3879        sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/searchbyday/*/*/*",
3880                INSTANCES_SEARCH_BY_DAY);
3881        sUriMatcher.addURI(CalendarContract.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS);
3882        sUriMatcher.addURI(CalendarContract.AUTHORITY, "events", EVENTS);
3883        sUriMatcher.addURI(CalendarContract.AUTHORITY, "events/#", EVENTS_ID);
3884        sUriMatcher.addURI(CalendarContract.AUTHORITY, "event_entities", EVENT_ENTITIES);
3885        sUriMatcher.addURI(CalendarContract.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID);
3886        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendars", CALENDARS);
3887        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendars/#", CALENDARS_ID);
3888        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_entities", CALENDAR_ENTITIES);
3889        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_entities/#", CALENDAR_ENTITIES_ID);
3890        sUriMatcher.addURI(CalendarContract.AUTHORITY, "attendees", ATTENDEES);
3891        sUriMatcher.addURI(CalendarContract.AUTHORITY, "attendees/#", ATTENDEES_ID);
3892        sUriMatcher.addURI(CalendarContract.AUTHORITY, "reminders", REMINDERS);
3893        sUriMatcher.addURI(CalendarContract.AUTHORITY, "reminders/#", REMINDERS_ID);
3894        sUriMatcher.addURI(CalendarContract.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES);
3895        sUriMatcher.addURI(CalendarContract.AUTHORITY, "extendedproperties/#",
3896                EXTENDED_PROPERTIES_ID);
3897        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS);
3898        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID);
3899        sUriMatcher.addURI(CalendarContract.AUTHORITY, "calendar_alerts/by_instance",
3900                           CALENDAR_ALERTS_BY_INSTANCE);
3901        sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate", SYNCSTATE);
3902        sUriMatcher.addURI(CalendarContract.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
3903        sUriMatcher.addURI(CalendarContract.AUTHORITY, CalendarAlarmManager.SCHEDULE_ALARM_PATH,
3904                SCHEDULE_ALARM);
3905        sUriMatcher.addURI(CalendarContract.AUTHORITY,
3906                CalendarAlarmManager.SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE);
3907        sUriMatcher.addURI(CalendarContract.AUTHORITY, "time/#", TIME);
3908        sUriMatcher.addURI(CalendarContract.AUTHORITY, "time", TIME);
3909        sUriMatcher.addURI(CalendarContract.AUTHORITY, "properties", PROVIDER_PROPERTIES);
3910        sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#", EXCEPTION_ID);
3911        sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#/#", EXCEPTION_ID2);
3912
3913        /** Contains just BaseColumns._COUNT */
3914        sCountProjectionMap = new HashMap<String, String>();
3915        sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
3916
3917        sEventsProjectionMap = new HashMap<String, String>();
3918        // Events columns
3919        sEventsProjectionMap.put(Events.ACCOUNT_NAME, Events.ACCOUNT_NAME);
3920        sEventsProjectionMap.put(Events.ACCOUNT_TYPE, Events.ACCOUNT_TYPE);
3921        sEventsProjectionMap.put(Events.TITLE, Events.TITLE);
3922        sEventsProjectionMap.put(Events.EVENT_LOCATION, Events.EVENT_LOCATION);
3923        sEventsProjectionMap.put(Events.DESCRIPTION, Events.DESCRIPTION);
3924        sEventsProjectionMap.put(Events.STATUS, Events.STATUS);
3925        sEventsProjectionMap.put(Events.EVENT_COLOR, Events.EVENT_COLOR);
3926        sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, Events.SELF_ATTENDEE_STATUS);
3927        sEventsProjectionMap.put(Events.DTSTART, Events.DTSTART);
3928        sEventsProjectionMap.put(Events.DTEND, Events.DTEND);
3929        sEventsProjectionMap.put(Events.EVENT_TIMEZONE, Events.EVENT_TIMEZONE);
3930        sEventsProjectionMap.put(Events.EVENT_END_TIMEZONE, Events.EVENT_END_TIMEZONE);
3931        sEventsProjectionMap.put(Events.DURATION, Events.DURATION);
3932        sEventsProjectionMap.put(Events.ALL_DAY, Events.ALL_DAY);
3933        sEventsProjectionMap.put(Events.ACCESS_LEVEL, Events.ACCESS_LEVEL);
3934        sEventsProjectionMap.put(Events.AVAILABILITY, Events.AVAILABILITY);
3935        sEventsProjectionMap.put(Events.HAS_ALARM, Events.HAS_ALARM);
3936        sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, Events.HAS_EXTENDED_PROPERTIES);
3937        sEventsProjectionMap.put(Events.RRULE, Events.RRULE);
3938        sEventsProjectionMap.put(Events.RDATE, Events.RDATE);
3939        sEventsProjectionMap.put(Events.EXRULE, Events.EXRULE);
3940        sEventsProjectionMap.put(Events.EXDATE, Events.EXDATE);
3941        sEventsProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID);
3942        sEventsProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID);
3943        sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, Events.ORIGINAL_INSTANCE_TIME);
3944        sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY);
3945        sEventsProjectionMap.put(Events.LAST_DATE, Events.LAST_DATE);
3946        sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, Events.HAS_ATTENDEE_DATA);
3947        sEventsProjectionMap.put(Events.CALENDAR_ID, Events.CALENDAR_ID);
3948        sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, Events.GUESTS_CAN_INVITE_OTHERS);
3949        sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, Events.GUESTS_CAN_MODIFY);
3950        sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, Events.GUESTS_CAN_SEE_GUESTS);
3951        sEventsProjectionMap.put(Events.ORGANIZER, Events.ORGANIZER);
3952        sEventsProjectionMap.put(Events.DELETED, Events.DELETED);
3953        sEventsProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID);
3954
3955        // Put the shared items into the Attendees, Reminders projection map
3956        sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3957        sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3958
3959        // Calendar columns
3960        sEventsProjectionMap.put(Calendars.CALENDAR_COLOR, Calendars.CALENDAR_COLOR);
3961        sEventsProjectionMap.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CALENDAR_ACCESS_LEVEL);
3962        sEventsProjectionMap.put(Calendars.VISIBLE, Calendars.VISIBLE);
3963        sEventsProjectionMap.put(Calendars.CALENDAR_TIME_ZONE, Calendars.CALENDAR_TIME_ZONE);
3964        sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, Calendars.OWNER_ACCOUNT);
3965        sEventsProjectionMap.put(Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_DISPLAY_NAME);
3966        sEventsProjectionMap.put(Calendars.ALLOWED_REMINDERS, Calendars.ALLOWED_REMINDERS);
3967        sEventsProjectionMap.put(Calendars.MAX_REMINDERS, Calendars.MAX_REMINDERS);
3968        sEventsProjectionMap.put(Calendars.CAN_ORGANIZER_RESPOND, Calendars.CAN_ORGANIZER_RESPOND);
3969        sEventsProjectionMap.put(Calendars.CAN_MODIFY_TIME_ZONE, Calendars.CAN_MODIFY_TIME_ZONE);
3970
3971        // Put the shared items into the Instances projection map
3972        // The Instances and CalendarAlerts are joined with Calendars, so the projections include
3973        // the above Calendar columns.
3974        sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3975        sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3976
3977        sEventsProjectionMap.put(Events._ID, Events._ID);
3978        sEventsProjectionMap.put(Events.SYNC_DATA1, Events.SYNC_DATA1);
3979        sEventsProjectionMap.put(Events.SYNC_DATA2, Events.SYNC_DATA2);
3980        sEventsProjectionMap.put(Events.SYNC_DATA3, Events.SYNC_DATA3);
3981        sEventsProjectionMap.put(Events.SYNC_DATA4, Events.SYNC_DATA4);
3982        sEventsProjectionMap.put(Events.SYNC_DATA5, Events.SYNC_DATA5);
3983        sEventsProjectionMap.put(Events.SYNC_DATA6, Events.SYNC_DATA6);
3984        sEventsProjectionMap.put(Events.SYNC_DATA7, Events.SYNC_DATA7);
3985        sEventsProjectionMap.put(Events.SYNC_DATA8, Events.SYNC_DATA8);
3986        sEventsProjectionMap.put(Events.SYNC_DATA9, Events.SYNC_DATA9);
3987        sEventsProjectionMap.put(Events.SYNC_DATA10, Events.SYNC_DATA10);
3988        sEventsProjectionMap.put(Calendars.CAL_SYNC1, Calendars.CAL_SYNC1);
3989        sEventsProjectionMap.put(Calendars.CAL_SYNC2, Calendars.CAL_SYNC2);
3990        sEventsProjectionMap.put(Calendars.CAL_SYNC3, Calendars.CAL_SYNC3);
3991        sEventsProjectionMap.put(Calendars.CAL_SYNC4, Calendars.CAL_SYNC4);
3992        sEventsProjectionMap.put(Calendars.CAL_SYNC5, Calendars.CAL_SYNC5);
3993        sEventsProjectionMap.put(Calendars.CAL_SYNC6, Calendars.CAL_SYNC6);
3994        sEventsProjectionMap.put(Calendars.CAL_SYNC7, Calendars.CAL_SYNC7);
3995        sEventsProjectionMap.put(Calendars.CAL_SYNC8, Calendars.CAL_SYNC8);
3996        sEventsProjectionMap.put(Calendars.CAL_SYNC9, Calendars.CAL_SYNC9);
3997        sEventsProjectionMap.put(Calendars.CAL_SYNC10, Calendars.CAL_SYNC10);
3998        sEventsProjectionMap.put(Events.DIRTY, Events.DIRTY);
3999        sEventsProjectionMap.put(Events.LAST_SYNCED, Events.LAST_SYNCED);
4000
4001        sEventEntitiesProjectionMap = new HashMap<String, String>();
4002        sEventEntitiesProjectionMap.put(Events.TITLE, Events.TITLE);
4003        sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, Events.EVENT_LOCATION);
4004        sEventEntitiesProjectionMap.put(Events.DESCRIPTION, Events.DESCRIPTION);
4005        sEventEntitiesProjectionMap.put(Events.STATUS, Events.STATUS);
4006        sEventEntitiesProjectionMap.put(Events.EVENT_COLOR, Events.EVENT_COLOR);
4007        sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, Events.SELF_ATTENDEE_STATUS);
4008        sEventEntitiesProjectionMap.put(Events.DTSTART, Events.DTSTART);
4009        sEventEntitiesProjectionMap.put(Events.DTEND, Events.DTEND);
4010        sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, Events.EVENT_TIMEZONE);
4011        sEventEntitiesProjectionMap.put(Events.EVENT_END_TIMEZONE, Events.EVENT_END_TIMEZONE);
4012        sEventEntitiesProjectionMap.put(Events.DURATION, Events.DURATION);
4013        sEventEntitiesProjectionMap.put(Events.ALL_DAY, Events.ALL_DAY);
4014        sEventEntitiesProjectionMap.put(Events.ACCESS_LEVEL, Events.ACCESS_LEVEL);
4015        sEventEntitiesProjectionMap.put(Events.AVAILABILITY, Events.AVAILABILITY);
4016        sEventEntitiesProjectionMap.put(Events.HAS_ALARM, Events.HAS_ALARM);
4017        sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES,
4018                Events.HAS_EXTENDED_PROPERTIES);
4019        sEventEntitiesProjectionMap.put(Events.RRULE, Events.RRULE);
4020        sEventEntitiesProjectionMap.put(Events.RDATE, Events.RDATE);
4021        sEventEntitiesProjectionMap.put(Events.EXRULE, Events.EXRULE);
4022        sEventEntitiesProjectionMap.put(Events.EXDATE, Events.EXDATE);
4023        sEventEntitiesProjectionMap.put(Events.ORIGINAL_SYNC_ID, Events.ORIGINAL_SYNC_ID);
4024        sEventEntitiesProjectionMap.put(Events.ORIGINAL_ID, Events.ORIGINAL_ID);
4025        sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME,
4026                Events.ORIGINAL_INSTANCE_TIME);
4027        sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, Events.ORIGINAL_ALL_DAY);
4028        sEventEntitiesProjectionMap.put(Events.LAST_DATE, Events.LAST_DATE);
4029        sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, Events.HAS_ATTENDEE_DATA);
4030        sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, Events.CALENDAR_ID);
4031        sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS,
4032                Events.GUESTS_CAN_INVITE_OTHERS);
4033        sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, Events.GUESTS_CAN_MODIFY);
4034        sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, Events.GUESTS_CAN_SEE_GUESTS);
4035        sEventEntitiesProjectionMap.put(Events.ORGANIZER, Events.ORGANIZER);
4036        sEventEntitiesProjectionMap.put(Events.DELETED, Events.DELETED);
4037        sEventEntitiesProjectionMap.put(Events._ID, Events._ID);
4038        sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID);
4039        sEventEntitiesProjectionMap.put(Events.SYNC_DATA1, Events.SYNC_DATA1);
4040        sEventEntitiesProjectionMap.put(Events.SYNC_DATA2, Events.SYNC_DATA2);
4041        sEventEntitiesProjectionMap.put(Events.SYNC_DATA3, Events.SYNC_DATA3);
4042        sEventEntitiesProjectionMap.put(Events.SYNC_DATA4, Events.SYNC_DATA4);
4043        sEventEntitiesProjectionMap.put(Events.SYNC_DATA5, Events.SYNC_DATA5);
4044        sEventEntitiesProjectionMap.put(Events.SYNC_DATA6, Events.SYNC_DATA6);
4045        sEventEntitiesProjectionMap.put(Events.SYNC_DATA7, Events.SYNC_DATA7);
4046        sEventEntitiesProjectionMap.put(Events.SYNC_DATA8, Events.SYNC_DATA8);
4047        sEventEntitiesProjectionMap.put(Events.SYNC_DATA9, Events.SYNC_DATA9);
4048        sEventEntitiesProjectionMap.put(Events.SYNC_DATA10, Events.SYNC_DATA10);
4049        sEventEntitiesProjectionMap.put(Events.DIRTY, Events.DIRTY);
4050        sEventEntitiesProjectionMap.put(Events.LAST_SYNCED, Events.LAST_SYNCED);
4051        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC1, Calendars.CAL_SYNC1);
4052        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC2, Calendars.CAL_SYNC2);
4053        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC3, Calendars.CAL_SYNC3);
4054        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC4, Calendars.CAL_SYNC4);
4055        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC5, Calendars.CAL_SYNC5);
4056        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC6, Calendars.CAL_SYNC6);
4057        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC7, Calendars.CAL_SYNC7);
4058        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC8, Calendars.CAL_SYNC8);
4059        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC9, Calendars.CAL_SYNC9);
4060        sEventEntitiesProjectionMap.put(Calendars.CAL_SYNC10, Calendars.CAL_SYNC10);
4061
4062        // Instances columns
4063        sInstancesProjectionMap.put(Events.DELETED, "Events.deleted as deleted");
4064        sInstancesProjectionMap.put(Instances.BEGIN, "begin");
4065        sInstancesProjectionMap.put(Instances.END, "end");
4066        sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
4067        sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
4068        sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
4069        sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
4070        sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
4071        sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
4072
4073        // Attendees columns
4074        sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
4075        sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
4076        sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
4077        sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
4078        sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
4079        sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
4080        sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
4081        sAttendeesProjectionMap.put(Events.DELETED, "Events.deleted AS deleted");
4082        sAttendeesProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id");
4083
4084        // Reminders columns
4085        sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
4086        sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
4087        sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
4088        sRemindersProjectionMap.put(Reminders.METHOD, "method");
4089
4090        // CalendarAlerts columns
4091        sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
4092        sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
4093        sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
4094        sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
4095        sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
4096        sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
4097        sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
4098
4099        // CalendarCache columns
4100        sCalendarCacheProjectionMap = new HashMap<String, String>();
4101        sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key");
4102        sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value");
4103    }
4104
4105    /**
4106     * Make sure that there are no entries for accounts that no longer
4107     * exist. We are overriding this since we need to delete from the
4108     * Calendars table, which is not syncable, which has triggers that
4109     * will delete from the Events and  tables, which are
4110     * syncable.  TODO: update comment, make sure deletes don't get synced.
4111     */
4112    @Override
4113    public void onAccountsUpdated(Account[] accounts) {
4114        if (mDb == null) {
4115            mDb = mDbHelper.getWritableDatabase();
4116        }
4117        if (mDb == null) {
4118            return;
4119        }
4120
4121        HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>();
4122        HashSet<Account> validAccounts = new HashSet<Account>();
4123        for (Account account : accounts) {
4124            validAccounts.add(new Account(account.name, account.type));
4125            accountHasCalendar.put(account, false);
4126        }
4127        ArrayList<Account> accountsToDelete = new ArrayList<Account>();
4128
4129        mDb.beginTransaction();
4130        try {
4131
4132            for (String table : new String[]{Tables.CALENDARS}) {
4133                // Find all the accounts the calendar DB knows about, mark the ones that aren't
4134                // in the valid set for deletion.
4135                Cursor c = mDb.rawQuery("SELECT DISTINCT " +
4136                                            Calendars.ACCOUNT_NAME +
4137                                            "," +
4138                                            Calendars.ACCOUNT_TYPE +
4139                                        " FROM " + table, null);
4140                while (c.moveToNext()) {
4141                    // ACCOUNT_TYPE_LOCAL is to store calendars not associated
4142                    // with a system account. Typically, a calendar must be
4143                    // associated with an account on the device or it will be
4144                    // deleted.
4145                    if (c.getString(0) != null
4146                            && c.getString(1) != null
4147                            && !TextUtils.equals(c.getString(1),
4148                                    CalendarContract.ACCOUNT_TYPE_LOCAL)) {
4149                        Account currAccount = new Account(c.getString(0), c.getString(1));
4150                        if (!validAccounts.contains(currAccount)) {
4151                            accountsToDelete.add(currAccount);
4152                        }
4153                    }
4154                }
4155                c.close();
4156            }
4157
4158            for (Account account : accountsToDelete) {
4159                if (Log.isLoggable(TAG, Log.DEBUG)) {
4160                    Log.d(TAG, "removing data for removed account " + account);
4161                }
4162                String[] params = new String[]{account.name, account.type};
4163                mDb.execSQL(SQL_DELETE_FROM_CALENDARS, params);
4164            }
4165            mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
4166            mDb.setTransactionSuccessful();
4167        } finally {
4168            mDb.endTransaction();
4169        }
4170
4171        // make sure the widget reflects the account changes
4172        sendUpdateNotification(false);
4173    }
4174
4175    /**
4176     * Inserts an argument at the beginning of the selection arg list.
4177     *
4178     * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is
4179     * prepended to the user's where clause (combined with 'AND') to generate
4180     * the final where close, so arguments associated with the QueryBuilder are
4181     * prepended before any user selection args to keep them in the right order.
4182     */
4183    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
4184        if (selectionArgs == null) {
4185            return new String[] {arg};
4186        } else {
4187            int newLength = selectionArgs.length + 1;
4188            String[] newSelectionArgs = new String[newLength];
4189            newSelectionArgs[0] = arg;
4190            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
4191            return newSelectionArgs;
4192        }
4193    }
4194}
4195