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