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