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