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