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