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