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