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