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