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