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