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