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