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