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