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