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