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