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