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