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