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