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