• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.providers.calendar.CalendarDatabaseHelper.Tables;
21 import com.google.common.annotations.VisibleForTesting;
22 
23 import android.accounts.Account;
24 import android.accounts.AccountManager;
25 import android.accounts.OnAccountsUpdateListener;
26 import android.app.AlarmManager;
27 import android.app.PendingIntent;
28 import android.content.BroadcastReceiver;
29 import android.content.ContentResolver;
30 import android.content.ContentUris;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.IntentFilter;
35 import android.content.UriMatcher;
36 import android.database.Cursor;
37 import android.database.DatabaseUtils;
38 import android.database.SQLException;
39 import android.database.sqlite.SQLiteDatabase;
40 import android.database.sqlite.SQLiteQueryBuilder;
41 import android.net.Uri;
42 import android.os.Debug;
43 import android.os.Process;
44 import android.pim.EventRecurrence;
45 import android.pim.RecurrenceSet;
46 import android.provider.BaseColumns;
47 import android.provider.Calendar;
48 import android.provider.Calendar.Attendees;
49 import android.provider.Calendar.CalendarAlerts;
50 import android.provider.Calendar.Calendars;
51 import android.provider.Calendar.Events;
52 import android.provider.Calendar.Instances;
53 import android.provider.Calendar.Reminders;
54 import android.text.TextUtils;
55 import android.text.format.DateUtils;
56 import android.text.format.Time;
57 import android.util.Log;
58 import android.util.TimeFormatException;
59 import android.util.TimeUtils;
60 
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.HashMap;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Set;
67 import java.util.TimeZone;
68 
69 /**
70  * Calendar content provider. The contract between this provider and applications
71  * is defined in {@link android.provider.Calendar}.
72  */
73 public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
74 
75     private static final String TAG = "CalendarProvider2";
76 
77     private static final String TIMEZONE_GMT = "GMT";
78 
79     private static final boolean PROFILE = false;
80     private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
81 
82     private static final String INVALID_CALENDARALERTS_SELECTOR =
83             "_id IN (SELECT ca._id FROM CalendarAlerts AS ca"
84                     + " LEFT OUTER JOIN Instances USING (event_id, begin, end)"
85                     + " LEFT OUTER JOIN Reminders AS r ON"
86                     + " (ca.event_id=r.event_id AND ca.minutes=r.minutes)"
87                     + " WHERE Instances.begin ISNULL OR ca.alarmTime<?"
88                     + "   OR (r.minutes ISNULL AND ca.minutes<>0))";
89 
90     private static final String[] ID_ONLY_PROJECTION =
91             new String[] {Events._ID};
92 
93     private static final String[] EVENTS_PROJECTION = new String[] {
94             Events._SYNC_ID,
95             Events.RRULE,
96             Events.RDATE,
97             Events.ORIGINAL_EVENT,
98     };
99     private static final int EVENTS_SYNC_ID_INDEX = 0;
100     private static final int EVENTS_RRULE_INDEX = 1;
101     private static final int EVENTS_RDATE_INDEX = 2;
102     private static final int EVENTS_ORIGINAL_EVENT_INDEX = 3;
103 
104     private static final String[] ID_PROJECTION = new String[] {
105             Attendees._ID,
106             Attendees.EVENT_ID, // Assume these are the same for each table
107     };
108     private static final int ID_INDEX = 0;
109     private static final int EVENT_ID_INDEX = 1;
110 
111     /**
112      * Projection to query for correcting times in allDay events.
113      */
114     private static final String[] ALLDAY_TIME_PROJECTION = new String[] {
115         Events._ID,
116         Events.DTSTART,
117         Events.DTEND,
118         Events.DURATION
119     };
120     private static final int ALLDAY_ID_INDEX = 0;
121     private static final int ALLDAY_DTSTART_INDEX = 1;
122     private static final int ALLDAY_DTEND_INDEX = 2;
123     private static final int ALLDAY_DURATION_INDEX = 3;
124 
125     private static final int DAY_IN_SECONDS = 24 * 60 * 60;
126 
127     /**
128      * The cached copy of the CalendarMetaData database table.
129      * Make this "package private" instead of "private" so that test code
130      * can access it.
131      */
132     MetaData mMetaData;
133     CalendarCache mCalendarCache;
134 
135     private CalendarDatabaseHelper mDbHelper;
136 
137     private static final Uri SYNCSTATE_CONTENT_URI = Uri.parse("content://syncstate/state");
138     //
139     // SCHEDULE_ALARM_URI runs scheduleNextAlarm(false)
140     // SCHEDULE_ALARM_REMOVE_URI runs scheduleNextAlarm(true)
141     // TODO: use a service to schedule alarms rather than private URI
142     /* package */ static final String SCHEDULE_ALARM_PATH = "schedule_alarms";
143     /* package */ static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove";
144     /* package */ static final Uri SCHEDULE_ALARM_URI =
145             Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_PATH);
146     /* package */ static final Uri SCHEDULE_ALARM_REMOVE_URI =
147             Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH);
148 
149     // 5 second delay before updating alarms
150     private static final long ALARM_SCHEDULER_DELAY = 5000;
151 
152     // To determine if a recurrence exception originally overlapped the
153     // window, we need to assume a maximum duration, since we only know
154     // the original start time.
155     private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
156 
157     // The extended property name for storing an Event original Timezone.
158     // Due to an issue in Calendar Server restricting the length of the name we had to strip it down
159     // TODO - Better name would be:
160     // "com.android.providers.calendar.CalendarSyncAdapter#originalTimezone"
161     protected static final String EXT_PROP_ORIGINAL_TIMEZONE =
162         "CalendarSyncAdapter#originalTimezone";
163 
164     private static final String SQL_SELECT_EVENTSRAWTIMES = "SELECT " +
165             EventsRawTimesColumns.EVENT_ID + ", " +
166             EventsRawTimesColumns.DTSTART_2445 + ", " +
167             EventsRawTimesColumns.DTEND_2445 + ", " +
168             Events.EVENT_TIMEZONE +
169             " FROM " +
170             "EventsRawTimes" + ", " +
171             "Events" +
172             " WHERE " +
173             EventsRawTimesColumns.EVENT_ID + " = " + "Events." + Events._ID;
174 
175     private static final String SQL_SELECT_COUNT_FOR_SYNC_ID =
176             "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?";
177 
178     private static final String SQL_WHERE_ID = BaseColumns._ID + "=?";
179 
180     public static final class TimeRange {
181         public long begin;
182         public long end;
183         public boolean allDay;
184     }
185 
186     public static final class InstancesRange {
187         public long begin;
188         public long end;
189 
InstancesRange(long begin, long end)190         public InstancesRange(long begin, long end) {
191             this.begin = begin;
192             this.end = end;
193         }
194     }
195 
196     public static final class InstancesList
197             extends ArrayList<ContentValues> {
198     }
199 
200     public static final class EventInstancesMap
201             extends HashMap<String, InstancesList> {
add(String syncIdKey, ContentValues values)202         public void add(String syncIdKey, ContentValues values) {
203             InstancesList instances = get(syncIdKey);
204             if (instances == null) {
205                 instances = new InstancesList();
206                 put(syncIdKey, instances);
207             }
208             instances.add(values);
209         }
210     }
211 
212     // A thread that runs in the background and schedules the next
213     // calendar event alarm. It delays for 5 seconds before updating
214     // to aggregate further requests.
215     private class AlarmScheduler extends Thread {
216         boolean mRemoveAlarms;
217 
AlarmScheduler(boolean removeAlarms)218         public AlarmScheduler(boolean removeAlarms) {
219             mRemoveAlarms = removeAlarms;
220         }
221 
222         @Override
run()223         public void run() {
224             Context context = CalendarProvider2.this.getContext();
225             // Because the handler does not guarantee message delivery in
226             // the case that the provider is killed, we need to make sure
227             // that the provider stays alive long enough to deliver the
228             // notification. This empty service is sufficient to "wedge" the
229             // process until we finish.
230             context.startService(new Intent(context, EmptyService.class));
231             while (true) {
232                 // Wait a bit before writing to collect any other requests that
233                 // may come in
234                 try {
235                     sleep(ALARM_SCHEDULER_DELAY);
236                 } catch (InterruptedException e1) {
237                     if(Log.isLoggable(TAG, Log.DEBUG)) {
238                         Log.d(TAG, "AlarmScheduler woke up early: " + e1.getMessage());
239                     }
240                 }
241                 // Clear any new requests and update whether or not we should
242                 // remove alarms
243                 synchronized (mAlarmLock) {
244                     mRemoveAlarms = mRemoveAlarms || mRemoveAlarmsOnRerun;
245                     mRerunAlarmScheduler = false;
246                     mRemoveAlarmsOnRerun = false;
247                 }
248                 // Run the update
249                 try {
250                     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
251                     runScheduleNextAlarm(mRemoveAlarms);
252                 } catch (SQLException e) {
253                     if (Log.isLoggable(TAG, Log.ERROR)) {
254                         Log.e(TAG, "runScheduleNextAlarm() failed", e);
255                     }
256                 }
257                 // Check if anyone requested another alarm change while we were busy.
258                 // if not clear everything out and exit.
259                 synchronized (mAlarmLock) {
260                     if (!mRerunAlarmScheduler) {
261                         mAlarmScheduler = null;
262                         mRerunAlarmScheduler = false;
263                         mRemoveAlarmsOnRerun = false;
264                         context.stopService(new Intent(context, EmptyService.class));
265                         return;
266                     }
267                 }
268             }
269         }
270     }
271 
272     private static AlarmScheduler mAlarmScheduler;
273 
274     private static boolean mRerunAlarmScheduler = false;
275     private static boolean mRemoveAlarmsOnRerun = false;
276 
277     /**
278      * We search backward in time for event reminders that we may have missed
279      * and schedule them if the event has not yet expired.  The amount in
280      * the past to search backwards is controlled by this constant.  It
281      * should be at least a few minutes to allow for an event that was
282      * recently created on the web to make its way to the phone.  Two hours
283      * might seem like overkill, but it is useful in the case where the user
284      * just crossed into a new timezone and might have just missed an alarm.
285      */
286     private static final long SCHEDULE_ALARM_SLACK = 2 * DateUtils.HOUR_IN_MILLIS;
287 
288     /**
289      * Alarms older than this threshold will be deleted from the CalendarAlerts
290      * table.  This should be at least a day because if the timezone is
291      * wrong and the user corrects it we might delete good alarms that
292      * appear to be old because the device time was incorrectly in the future.
293      * This threshold must also be larger than SCHEDULE_ALARM_SLACK.  We add
294      * the SCHEDULE_ALARM_SLACK to ensure this.
295      *
296      * To make it easier to find and debug problems with missed reminders,
297      * set this to something greater than a day.
298      */
299     private static final long CLEAR_OLD_ALARM_THRESHOLD =
300             7 * DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK;
301 
302     // A lock for synchronizing access to fields that are shared
303     // with the AlarmScheduler thread.
304     private Object mAlarmLock = new Object();
305 
306     // Make sure we load at least two months worth of data.
307     // Client apps can load more data in a background thread.
308     private static final long MINIMUM_EXPANSION_SPAN =
309             2L * 31 * 24 * 60 * 60 * 1000;
310 
311     private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
312     private static final int CALENDARS_INDEX_ID = 0;
313 
314     // Allocate the string constant once here instead of on the heap
315     private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
316 
317     private static final String[] sInstancesProjection =
318             new String[] { Instances.START_DAY, Instances.END_DAY,
319                     Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
320 
321     private static final int INSTANCES_INDEX_START_DAY = 0;
322     private static final int INSTANCES_INDEX_END_DAY = 1;
323     private static final int INSTANCES_INDEX_START_MINUTE = 2;
324     private static final int INSTANCES_INDEX_END_MINUTE = 3;
325     private static final int INSTANCES_INDEX_ALL_DAY = 4;
326 
327     private AlarmManager mAlarmManager;
328 
329     private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
330 
331     /**
332      * Listens for timezone changes and disk-no-longer-full events
333      */
334     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
335         @Override
336         public void onReceive(Context context, Intent intent) {
337             String action = intent.getAction();
338             if (Log.isLoggable(TAG, Log.DEBUG)) {
339                 Log.d(TAG, "onReceive() " + action);
340             }
341             if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
342                 updateTimezoneDependentFields();
343                 scheduleNextAlarm(false /* do not remove alarms */);
344             } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
345                 // Try to clean up if things were screwy due to a full disk
346                 updateTimezoneDependentFields();
347                 scheduleNextAlarm(false /* do not remove alarms */);
348             } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
349                 scheduleNextAlarm(false /* do not remove alarms */);
350             }
351         }
352     };
353 
354     /**
355      * Columns from the EventsRawTimes table
356      */
357     public interface EventsRawTimesColumns
358     {
359         /**
360          * The corresponding event id
361          * <P>Type: INTEGER (long)</P>
362          */
363         public static final String EVENT_ID = "event_id";
364 
365         /**
366          * The RFC2445 compliant time the event starts
367          * <P>Type: TEXT</P>
368          */
369         public static final String DTSTART_2445 = "dtstart2445";
370 
371         /**
372          * The RFC2445 compliant time the event ends
373          * <P>Type: TEXT</P>
374          */
375         public static final String DTEND_2445 = "dtend2445";
376 
377         /**
378          * The RFC2445 compliant original instance time of the recurring event for which this
379          * event is an exception.
380          * <P>Type: TEXT</P>
381          */
382         public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445";
383 
384         /**
385          * The RFC2445 compliant last date this event repeats on, or NULL if it never ends
386          * <P>Type: TEXT</P>
387          */
388         public static final String LAST_DATE_2445 = "lastDate2445";
389     }
390 
verifyAccounts()391     protected void verifyAccounts() {
392         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
393         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
394     }
395 
396     /* Visible for testing */
397     @Override
getDatabaseHelper(final Context context)398     protected CalendarDatabaseHelper getDatabaseHelper(final Context context) {
399         return CalendarDatabaseHelper.getInstance(context);
400     }
401 
402     @Override
onCreate()403     public boolean onCreate() {
404         super.onCreate();
405         mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper();
406 
407         verifyAccounts();
408 
409         // Register for Intent broadcasts
410         IntentFilter filter = new IntentFilter();
411 
412         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
413         filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
414         filter.addAction(Intent.ACTION_TIME_CHANGED);
415         final Context c = getContext();
416 
417         // We don't ever unregister this because this thread always wants
418         // to receive notifications, even in the background.  And if this
419         // thread is killed then the whole process will be killed and the
420         // memory resources will be reclaimed.
421         c.registerReceiver(mIntentReceiver, filter);
422 
423         mMetaData = new MetaData(mDbHelper);
424         mCalendarCache = new CalendarCache(mDbHelper);
425 
426         updateTimezoneDependentFields();
427 
428         return true;
429     }
430 
431     /**
432      * This creates a background thread to check the timezone and update
433      * the timezone dependent fields in the Instances table if the timezone
434      * has changed.
435      */
updateTimezoneDependentFields()436     protected void updateTimezoneDependentFields() {
437         Thread thread = new TimezoneCheckerThread();
438         thread.start();
439     }
440 
441     private class TimezoneCheckerThread extends Thread {
442         @Override
run()443         public void run() {
444             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
445             try {
446                 doUpdateTimezoneDependentFields();
447                 triggerAppWidgetUpdate(-1 /*changedEventId*/ );
448             } catch (SQLException e) {
449                 if (Log.isLoggable(TAG, Log.ERROR)) {
450                     Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
451                 }
452                 try {
453                     // Clear at least the in-memory data (and if possible the
454                     // database fields) to force a re-computation of Instances.
455                     mMetaData.clearInstanceRange();
456                 } catch (SQLException e2) {
457                     if (Log.isLoggable(TAG, Log.ERROR)) {
458                         Log.e(TAG, "clearInstanceRange() also failed: " + e2);
459                     }
460                 }
461             }
462         }
463     }
464 
465     /**
466      * Check if we are in the same time zone
467      */
isLocalSameAsInstancesTimezone()468     private boolean isLocalSameAsInstancesTimezone() {
469         String localTimezone = TimeZone.getDefault().getID();
470         return TextUtils.equals(mCalendarCache.readTimezoneInstances(), localTimezone);
471     }
472 
473     /**
474      * This method runs in a background thread.  If the timezone db or timezone has changed
475      * then the Instances table will be regenerated.
476      */
doUpdateTimezoneDependentFields()477     protected void doUpdateTimezoneDependentFields() {
478         String timezoneType = mCalendarCache.readTimezoneType();
479         // Nothing to do if we have the "home" timezone type (timezone is sticky)
480         if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
481             return;
482         }
483         // We are here in "auto" mode, the timezone is coming from the device
484         if (! isSameTimezoneDatabaseVersion()) {
485             String localTimezone = TimeZone.getDefault().getID();
486             doProcessEventRawTimes(localTimezone, TimeUtils.getTimeZoneDatabaseVersion());
487         }
488         if (isLocalSameAsInstancesTimezone()) {
489             // Even if the timezone hasn't changed, check for missed alarms.
490             // This code executes when the CalendarProvider2 is created and
491             // helps to catch missed alarms when the Calendar process is
492             // killed (because of low-memory conditions) and then restarted.
493             rescheduleMissedAlarms();
494         }
495     }
496 
doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion)497     protected void doProcessEventRawTimes(String localTimezone, String timeZoneDatabaseVersion) {
498         mDb = mDbHelper.getWritableDatabase();
499         if (mDb == null) {
500             if (Log.isLoggable(TAG, Log.VERBOSE)) {
501                 Log.v(TAG, "Cannot update Events table from EventsRawTimes table");
502             }
503             return;
504         }
505         mDb.beginTransaction();
506         try {
507             updateEventsStartEndFromEventRawTimesLocked();
508             updateTimezoneDatabaseVersion(timeZoneDatabaseVersion);
509             mCalendarCache.writeTimezoneInstances(localTimezone);
510             regenerateInstancesTable();
511             mDb.setTransactionSuccessful();
512         } finally {
513             mDb.endTransaction();
514         }
515     }
516 
updateEventsStartEndFromEventRawTimesLocked()517     private void updateEventsStartEndFromEventRawTimesLocked() {
518         Cursor cursor = mDb.rawQuery(SQL_SELECT_EVENTSRAWTIMES, null /* selection args */);
519         try {
520             while (cursor.moveToNext()) {
521                 long eventId = cursor.getLong(0);
522                 String dtStart2445 = cursor.getString(1);
523                 String dtEnd2445 = cursor.getString(2);
524                 String eventTimezone = cursor.getString(3);
525                 if (dtStart2445 == null && dtEnd2445 == null) {
526                     if (Log.isLoggable(TAG, Log.ERROR)) {
527                         Log.e(TAG, "Event " + eventId + " has dtStart2445 and dtEnd2445 null "
528                                 + "at the same time in EventsRawTimes!");
529                     }
530                     continue;
531                 }
532                 updateEventsStartEndLocked(eventId,
533                         eventTimezone,
534                         dtStart2445,
535                         dtEnd2445);
536             }
537         } finally {
538             cursor.close();
539             cursor = null;
540         }
541     }
542 
get2445ToMillis(String timezone, String dt2445)543     private long get2445ToMillis(String timezone, String dt2445) {
544         if (null == dt2445) {
545             if (Log.isLoggable(TAG, Log.VERBOSE)) {
546                 Log.v( TAG, "Cannot parse null RFC2445 date");
547             }
548             return 0;
549         }
550         Time time = (timezone != null) ? new Time(timezone) : new Time();
551         try {
552             time.parse(dt2445);
553         } catch (TimeFormatException e) {
554             if (Log.isLoggable(TAG, Log.ERROR)) {
555                 Log.e( TAG, "Cannot parse RFC2445 date " + dt2445);
556             }
557             return 0;
558         }
559         return time.toMillis(true /* ignore DST */);
560     }
561 
updateEventsStartEndLocked(long eventId, String timezone, String dtStart2445, String dtEnd2445)562     private void updateEventsStartEndLocked(long eventId,
563             String timezone, String dtStart2445, String dtEnd2445) {
564 
565         ContentValues values = new ContentValues();
566         values.put("dtstart", get2445ToMillis(timezone, dtStart2445));
567         values.put("dtend", get2445ToMillis(timezone, dtEnd2445));
568 
569         int result = mDb.update("Events", values, "_id=?",
570                 new String[] {String.valueOf(eventId)});
571         if (0 == result) {
572             if (Log.isLoggable(TAG, Log.VERBOSE)) {
573                 Log.v(TAG, "Could not update Events table with values " + values);
574             }
575         }
576     }
577 
updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion)578     private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) {
579         try {
580             mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion);
581         } catch (CalendarCache.CacheException e) {
582             if (Log.isLoggable(TAG, Log.ERROR)) {
583                 Log.e(TAG, "Could not write timezone database version in the cache");
584             }
585         }
586     }
587 
588     /**
589      * Check if the time zone database version is the same as the cached one
590      */
isSameTimezoneDatabaseVersion()591     protected boolean isSameTimezoneDatabaseVersion() {
592         String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
593         if (timezoneDatabaseVersion == null) {
594             return false;
595         }
596         return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion());
597     }
598 
599     @VisibleForTesting
getTimezoneDatabaseVersion()600     protected String getTimezoneDatabaseVersion() {
601         String timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
602         if (timezoneDatabaseVersion == null) {
603             return "";
604         }
605         if (Log.isLoggable(TAG, Log.INFO)) {
606             Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion);
607         }
608         return timezoneDatabaseVersion;
609     }
610 
isHomeTimezone()611     private boolean isHomeTimezone() {
612         String type = mCalendarCache.readTimezoneType();
613         return type.equals(CalendarCache.TIMEZONE_TYPE_HOME);
614     }
615 
regenerateInstancesTable()616     private void regenerateInstancesTable() {
617         // The database timezone is different from the current timezone.
618         // Regenerate the Instances table for this month.  Include events
619         // starting at the beginning of this month.
620         long now = System.currentTimeMillis();
621         String instancesTimezone = mCalendarCache.readTimezoneInstances();
622         Time time = new Time(instancesTimezone);
623         time.set(now);
624         time.monthDay = 1;
625         time.hour = 0;
626         time.minute = 0;
627         time.second = 0;
628 
629         long begin = time.normalize(true);
630         long end = begin + MINIMUM_EXPANSION_SPAN;
631 
632         Cursor cursor = null;
633         try {
634             cursor = handleInstanceQuery(new SQLiteQueryBuilder(),
635                     begin, end,
636                     new String[] { Instances._ID },
637                     null /* selection */, null /* sort */,
638                     false /* searchByDayInsteadOfMillis */,
639                     true /* force Instances deletion and expansion */,
640                     instancesTimezone,
641                     isHomeTimezone());
642         } finally {
643             if (cursor != null) {
644                 cursor.close();
645             }
646         }
647 
648         rescheduleMissedAlarms();
649     }
650 
rescheduleMissedAlarms()651     private void rescheduleMissedAlarms() {
652         AlarmManager manager = getAlarmManager();
653         if (manager != null) {
654             Context context = getContext();
655             ContentResolver cr = context.getContentResolver();
656             CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
657         }
658     }
659 
660     /**
661      * Appends comma separated ids.
662      * @param ids Should not be empty
663      */
appendIds(StringBuilder sb, HashSet<Long> ids)664     private void appendIds(StringBuilder sb, HashSet<Long> ids) {
665         for (long id : ids) {
666             sb.append(id).append(',');
667         }
668 
669         sb.setLength(sb.length() - 1); // Yank the last comma
670     }
671 
672     @Override
notifyChange()673     protected void notifyChange() {
674         // Note that semantics are changed: notification is for CONTENT_URI, not the specific
675         // Uri that was modified.
676         getContext().getContentResolver().notifyChange(Calendar.CONTENT_URI, null,
677                 true /* syncToNetwork */);
678     }
679 
680     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)681     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
682             String sortOrder) {
683         if (Log.isLoggable(TAG, Log.VERBOSE)) {
684             Log.v(TAG, "query uri - " + uri);
685         }
686 
687         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
688 
689         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
690         String groupBy = null;
691         String limit = null; // Not currently implemented
692         String instancesTimezone;
693 
694         final int match = sUriMatcher.match(uri);
695         switch (match) {
696             case SYNCSTATE:
697                 return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
698                         sortOrder);
699 
700             case EVENTS:
701                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
702                 qb.setProjectionMap(sEventsProjectionMap);
703                 appendAccountFromParameter(qb, uri);
704                 break;
705             case EVENTS_ID:
706                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
707                 qb.setProjectionMap(sEventsProjectionMap);
708                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
709                 qb.appendWhere("_id=?");
710                 break;
711 
712             case EVENT_ENTITIES:
713                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
714                 qb.setProjectionMap(sEventEntitiesProjectionMap);
715                 appendAccountFromParameter(qb, uri);
716                 break;
717             case EVENT_ENTITIES_ID:
718                 qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
719                 qb.setProjectionMap(sEventEntitiesProjectionMap);
720                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
721                 qb.appendWhere("_id=?");
722                 break;
723 
724             case CALENDARS:
725                 qb.setTables("Calendars");
726                 appendAccountFromParameter(qb, uri);
727                 break;
728             case CALENDARS_ID:
729                 qb.setTables("Calendars");
730                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
731                 qb.appendWhere("_id=?");
732                 break;
733             case INSTANCES:
734             case INSTANCES_BY_DAY:
735                 long begin;
736                 long end;
737                 try {
738                     begin = Long.valueOf(uri.getPathSegments().get(2));
739                 } catch (NumberFormatException nfe) {
740                     throw new IllegalArgumentException("Cannot parse begin "
741                             + uri.getPathSegments().get(2));
742                 }
743                 try {
744                     end = Long.valueOf(uri.getPathSegments().get(3));
745                 } catch (NumberFormatException nfe) {
746                     throw new IllegalArgumentException("Cannot parse end "
747                             + uri.getPathSegments().get(3));
748                 }
749                 instancesTimezone = mCalendarCache.readTimezoneInstances();
750                 return handleInstanceQuery(qb, begin, end, projection,
751                         selection, sortOrder, match == INSTANCES_BY_DAY,
752                         false /* do not force Instances deletion and expansion */,
753                         instancesTimezone, isHomeTimezone());
754             case EVENT_DAYS:
755                 int startDay;
756                 int endDay;
757                 try {
758                     startDay = Integer.valueOf(uri.getPathSegments().get(2));
759                 } catch (NumberFormatException nfe) {
760                     throw new IllegalArgumentException("Cannot parse start day "
761                             + uri.getPathSegments().get(2));
762                 }
763                 try {
764                     endDay = Integer.valueOf(uri.getPathSegments().get(3));
765                 } catch (NumberFormatException nfe) {
766                     throw new IllegalArgumentException("Cannot parse end day "
767                             + uri.getPathSegments().get(3));
768                 }
769                 instancesTimezone = mCalendarCache.readTimezoneInstances();
770                 return handleEventDayQuery(qb, startDay, endDay, projection, selection,
771                         instancesTimezone, isHomeTimezone());
772             case ATTENDEES:
773                 qb.setTables("Attendees, Events");
774                 qb.setProjectionMap(sAttendeesProjectionMap);
775                 qb.appendWhere("Events._id=Attendees.event_id");
776                 break;
777             case ATTENDEES_ID:
778                 qb.setTables("Attendees, Events");
779                 qb.setProjectionMap(sAttendeesProjectionMap);
780                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
781                 qb.appendWhere("Attendees._id=?  AND Events._id=Attendees.event_id");
782                 break;
783             case REMINDERS:
784                 qb.setTables("Reminders");
785                 break;
786             case REMINDERS_ID:
787                 qb.setTables("Reminders, Events");
788                 qb.setProjectionMap(sRemindersProjectionMap);
789                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
790                 qb.appendWhere("Reminders._id=? AND Events._id=Reminders.event_id");
791                 break;
792             case CALENDAR_ALERTS:
793                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
794                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
795                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
796                         "._id=CalendarAlerts.event_id");
797                 break;
798             case CALENDAR_ALERTS_BY_INSTANCE:
799                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
800                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
801                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
802                         "._id=CalendarAlerts.event_id");
803                 groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
804                 break;
805             case CALENDAR_ALERTS_ID:
806                 qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
807                 qb.setProjectionMap(sCalendarAlertsProjectionMap);
808                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
809                 qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
810                         "._id=CalendarAlerts.event_id AND CalendarAlerts._id=?");
811                 break;
812             case EXTENDED_PROPERTIES:
813                 qb.setTables("ExtendedProperties");
814                 break;
815             case EXTENDED_PROPERTIES_ID:
816                 qb.setTables("ExtendedProperties");
817                 selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
818                 qb.appendWhere("ExtendedProperties._id=?");
819                 break;
820             case PROVIDER_PROPERTIES:
821                 qb.setTables("CalendarCache");
822                 qb.setProjectionMap(sCalendarCacheProjectionMap);
823                 break;
824             default:
825                 throw new IllegalArgumentException("Unknown URL " + uri);
826         }
827 
828         // run the query
829         return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
830     }
831 
query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit)832     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
833             String selection, String[] selectionArgs, String sortOrder, String groupBy,
834             String limit) {
835 
836         if (Log.isLoggable(TAG, Log.VERBOSE)) {
837             Log.v(TAG, "query sql - projection: " + Arrays.toString(projection) +
838                     " selection: " + selection +
839                     " selectionArgs: " + Arrays.toString(selectionArgs) +
840                     " sortOrder: " + sortOrder +
841                     " groupBy: " + groupBy +
842                     " limit: " + limit);
843         }
844         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
845                 sortOrder, limit);
846         if (c != null) {
847             // TODO: is this the right notification Uri?
848             c.setNotificationUri(getContext().getContentResolver(), Calendar.Events.CONTENT_URI);
849         }
850         return c;
851     }
852 
853     /*
854      * Fills the Instances table, if necessary, for the given range and then
855      * queries the Instances table.
856      *
857      * @param qb The query
858      * @param rangeBegin start of range (Julian days or ms)
859      * @param rangeEnd end of range (Julian days or ms)
860      * @param projection The projection
861      * @param selection The selection
862      * @param sort How to sort
863      * @param searchByDay if true, range is in Julian days, if false, range is in ms
864      * @param forceExpansion force the Instance deletion and expansion if set to true
865      * @param instancesTimezone timezone we need to use for computing the instances
866      * @param isHomeTimezone if true, we are in the "home" timezone
867      * @return
868      */
handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin, long rangeEnd, String[] projection, String selection, String sort, boolean searchByDay, boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone)869     private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
870             long rangeEnd, String[] projection, String selection, String sort,
871             boolean searchByDay, boolean forceExpansion, String instancesTimezone,
872             boolean isHomeTimezone) {
873 
874         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
875                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
876         qb.setProjectionMap(sInstancesProjectionMap);
877         if (searchByDay) {
878             // Convert the first and last Julian day range to a range that uses
879             // UTC milliseconds.
880             Time time = new Time(instancesTimezone);
881             long beginMs = time.setJulianDay((int) rangeBegin);
882             // We add one to lastDay because the time is set to 12am on the given
883             // Julian day and we want to include all the events on the last day.
884             long endMs = time.setJulianDay((int) rangeEnd + 1);
885             // will lock the database.
886             acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */,
887                     forceExpansion, instancesTimezone, isHomeTimezone
888             );
889             qb.appendWhere("startDay<=? AND endDay>=?");
890         } else {
891             // will lock the database.
892             acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */,
893                     forceExpansion, instancesTimezone, isHomeTimezone
894             );
895             qb.appendWhere("begin<=? AND end>=?");
896         }
897         String selectionArgs[] = new String[] {String.valueOf(rangeEnd),
898                 String.valueOf(rangeBegin)};
899         return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */,
900                 null /* having */, sort);
901     }
902 
handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end, String[] projection, String selection, String instancesTimezone, boolean isHomeTimezone)903     private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end,
904             String[] projection, String selection, String instancesTimezone,
905             boolean isHomeTimezone) {
906         qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
907                 "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
908         qb.setProjectionMap(sInstancesProjectionMap);
909         // Convert the first and last Julian day range to a range that uses
910         // UTC milliseconds.
911         Time time = new Time(instancesTimezone);
912         long beginMs = time.setJulianDay(begin);
913         // We add one to lastDay because the time is set to 12am on the given
914         // Julian day and we want to include all the events on the last day.
915         long endMs = time.setJulianDay(end + 1);
916 
917         acquireInstanceRange(beginMs, endMs, true,
918                 false /* do not force Instances expansion */, instancesTimezone, isHomeTimezone);
919         qb.appendWhere("startDay<=? AND endDay>=?");
920         String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)};
921 
922         return qb.query(mDb, projection, selection, selectionArgs,
923                 Instances.START_DAY /* groupBy */, null /* having */, null);
924     }
925 
926     /**
927      * Ensure that the date range given has all elements in the instance
928      * table.  Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
929      *
930      * @param begin start of range (ms)
931      * @param end end of range (ms)
932      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
933      * @param forceExpansion force the Instance deletion and expansion if set to true
934      * @param instancesTimezone timezone we need to use for computing the instances
935      * @param isHomeTimezone if true, we are in the "home" timezone
936      */
acquireInstanceRange(final long begin, final long end, final boolean useMinimumExpansionWindow, final boolean forceExpansion, final String instancesTimezone, final boolean isHomeTimezone)937     private void acquireInstanceRange(final long begin, final long end,
938             final boolean useMinimumExpansionWindow, final boolean forceExpansion,
939             final String instancesTimezone, final boolean isHomeTimezone) {
940         mDb.beginTransaction();
941         try {
942             acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow,
943                     forceExpansion, instancesTimezone, isHomeTimezone);
944             mDb.setTransactionSuccessful();
945         } finally {
946             mDb.endTransaction();
947         }
948     }
949 
950     /**
951      * Ensure that the date range given has all elements in the instance
952      * table.  The database lock must be held when calling this method.
953      *
954      * @param begin start of range (ms)
955      * @param end end of range (ms)
956      * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
957      * @param forceExpansion force the Instance deletion and expansion if set to true
958      * @param instancesTimezone timezone we need to use for computing the instances
959      * @param isHomeTimezone if true, we are in the "home" timezone
960      */
acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow, boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone)961     private void acquireInstanceRangeLocked(long begin, long end, boolean useMinimumExpansionWindow,
962             boolean forceExpansion, String instancesTimezone, boolean isHomeTimezone) {
963         long expandBegin = begin;
964         long expandEnd = end;
965 
966         if (instancesTimezone == null) {
967             if (Log.isLoggable(TAG, Log.ERROR)) {
968                 Log.e(TAG, "Cannot run acquireInstanceRangeLocked() "
969                         + "because instancesTimezone is null");
970             }
971             return;
972         }
973 
974         if (useMinimumExpansionWindow) {
975             // if we end up having to expand events into the instances table, expand
976             // events for a minimal amount of time, so we do not have to perform
977             // expansions frequently.
978             long span = end - begin;
979             if (span < MINIMUM_EXPANSION_SPAN) {
980                 long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
981                 expandBegin -= additionalRange;
982                 expandEnd += additionalRange;
983             }
984         }
985 
986         // Check if the timezone has changed.
987         // We do this check here because the database is locked and we can
988         // safely delete all the entries in the Instances table.
989         MetaData.Fields fields = mMetaData.getFieldsLocked();
990         long maxInstance = fields.maxInstance;
991         long minInstance = fields.minInstance;
992         boolean timezoneChanged;
993         if (isHomeTimezone) {
994             String previousTimezone = mCalendarCache.readTimezoneInstancesPrevious();
995             timezoneChanged = !instancesTimezone.equals(previousTimezone);
996         } else {
997             String localTimezone = TimeZone.getDefault().getID();
998             timezoneChanged = !instancesTimezone.equals(localTimezone);
999             // if we're in auto make sure we are using the device time zone
1000             if (timezoneChanged) {
1001                 instancesTimezone = localTimezone;
1002             }
1003         }
1004         // if "home", then timezoneChanged only if current != previous
1005         // if "auto", then timezoneChanged, if !instancesTimezone.equals(localTimezone);
1006         if (maxInstance == 0 || timezoneChanged || forceExpansion) {
1007             // Empty the Instances table and expand from scratch.
1008             mDb.execSQL("DELETE FROM Instances;");
1009             if (Log.isLoggable(TAG, Log.VERBOSE)) {
1010                 Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances,"
1011                         + " timezone changed: " + timezoneChanged);
1012             }
1013             expandInstanceRangeLocked(expandBegin, expandEnd, instancesTimezone);
1014 
1015             mMetaData.writeLocked(instancesTimezone, expandBegin, expandEnd);
1016 
1017             String timezoneType = mCalendarCache.readTimezoneType();
1018             // This may cause some double writes but guarantees the time zone in
1019             // the db and the time zone the instances are in is the same, which
1020             // future changes may affect.
1021             mCalendarCache.writeTimezoneInstances(instancesTimezone);
1022 
1023             // If we're in auto check if we need to fix the previous tz value
1024             if (timezoneType.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
1025                 String prevTZ = mCalendarCache.readTimezoneInstancesPrevious();
1026                 if (TextUtils.equals(TIMEZONE_GMT, prevTZ)) {
1027                     mCalendarCache.writeTimezoneInstancesPrevious(instancesTimezone);
1028                 }
1029             }
1030             return;
1031         }
1032 
1033         // If the desired range [begin, end] has already been
1034         // expanded, then simply return.  The range is inclusive, that is,
1035         // events that touch either endpoint are included in the expansion.
1036         // This means that a zero-duration event that starts and ends at
1037         // the endpoint will be included.
1038         // We use [begin, end] here and not [expandBegin, expandEnd] for
1039         // checking the range because a common case is for the client to
1040         // request successive days or weeks, for example.  If we checked
1041         // that the expanded range [expandBegin, expandEnd] then we would
1042         // always be expanding because there would always be one more day
1043         // or week that hasn't been expanded.
1044         if ((begin >= minInstance) && (end <= maxInstance)) {
1045             if (Log.isLoggable(TAG, Log.VERBOSE)) {
1046                 Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
1047                         + ") falls within previously expanded range.");
1048             }
1049             return;
1050         }
1051 
1052         // If the requested begin point has not been expanded, then include
1053         // more events than requested in the expansion (use "expandBegin").
1054         if (begin < minInstance) {
1055             expandInstanceRangeLocked(expandBegin, minInstance, instancesTimezone);
1056             minInstance = expandBegin;
1057         }
1058 
1059         // If the requested end point has not been expanded, then include
1060         // more events than requested in the expansion (use "expandEnd").
1061         if (end > maxInstance) {
1062             expandInstanceRangeLocked(maxInstance, expandEnd, instancesTimezone);
1063             maxInstance = expandEnd;
1064         }
1065 
1066         // Update the bounds on the Instances table (timezone is the same here)
1067         mMetaData.writeLocked(instancesTimezone, minInstance, maxInstance);
1068     }
1069 
1070     private static final String[] EXPAND_COLUMNS = new String[] {
1071             Events._ID,
1072             Events._SYNC_ID,
1073             Events.STATUS,
1074             Events.DTSTART,
1075             Events.DTEND,
1076             Events.EVENT_TIMEZONE,
1077             Events.RRULE,
1078             Events.RDATE,
1079             Events.EXRULE,
1080             Events.EXDATE,
1081             Events.DURATION,
1082             Events.ALL_DAY,
1083             Events.ORIGINAL_EVENT,
1084             Events.ORIGINAL_INSTANCE_TIME,
1085             Events.CALENDAR_ID,
1086             Events.DELETED
1087     };
1088 
1089     /**
1090      * Make instances for the given range.
1091      */
expandInstanceRangeLocked(long begin, long end, String localTimezone)1092     private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
1093 
1094         if (PROFILE) {
1095             Debug.startMethodTracing("expandInstanceRangeLocked");
1096         }
1097 
1098         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1099             Log.v(TAG, "Expanding events between " + begin + " and " + end);
1100         }
1101 
1102         Cursor entries = getEntries(begin, end);
1103         try {
1104             performInstanceExpansion(begin, end, localTimezone, entries);
1105         } finally {
1106             if (entries != null) {
1107                 entries.close();
1108             }
1109         }
1110         if (PROFILE) {
1111             Debug.stopMethodTracing();
1112         }
1113     }
1114 
1115     /**
1116      * Get all entries affecting the given window.
1117      * @param begin Window start (ms).
1118      * @param end Window end (ms).
1119      * @return Cursor for the entries; caller must close it.
1120      */
getEntries(long begin, long end)1121     private Cursor getEntries(long begin, long end) {
1122         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
1123         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
1124         qb.setProjectionMap(sEventsProjectionMap);
1125 
1126         String beginString = String.valueOf(begin);
1127         String endString = String.valueOf(end);
1128 
1129         // grab recurrence exceptions that fall outside our expansion window but modify
1130         // recurrences that do fall within our window.  we won't insert these into the output
1131         // set of instances, but instead will just add them to our cancellations list, so we
1132         // can cancel the correct recurrence expansion instances.
1133         // we don't have originalInstanceDuration or end time.  for now, assume the original
1134         // instance lasts no longer than 1 week.
1135         // also filter with syncable state (we dont want the entries from a non syncable account)
1136         // TODO: compute the originalInstanceEndTime or get this from the server.
1137         qb.appendWhere("((dtstart <= ? AND (lastDate IS NULL OR lastDate >= ?)) OR " +
1138                 "(originalInstanceTime IS NOT NULL AND originalInstanceTime <= ? AND " +
1139                 "originalInstanceTime >= ?)) AND (sync_events != 0)");
1140         String selectionArgs[] = new String[] {endString, beginString, endString,
1141                 String.valueOf(begin - MAX_ASSUMED_DURATION)};
1142         Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */,
1143                 selectionArgs, null /* groupBy */,
1144                 null /* having */, null /* sortOrder */);
1145         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1146             Log.v(TAG, "Instance expansion:  got " + c.getCount() + " entries");
1147         }
1148         return c;
1149     }
1150 
1151     /**
1152      * Generates a unique key from the syncId and calendarId.
1153      * The purpose of this is to prevent collisions if two different calendars use the
1154      * same sync id.  This can happen if a Google calendar is accessed by two different accounts,
1155      * or with Exchange, where ids are not unique between calendars.
1156      * @param syncId Id for the event
1157      * @param calendarId Id for the calendar
1158      * @return key
1159      */
getSyncIdKey(String syncId, long calendarId)1160     private String getSyncIdKey(String syncId, long calendarId) {
1161         return calendarId + ":" + syncId;
1162     }
1163 
1164     /**
1165      * Perform instance expansion on the given entries.
1166      * @param begin Window start (ms).
1167      * @param end Window end (ms).
1168      * @param localTimezone
1169      * @param entries The entries to process.
1170      */
performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries)1171     private void performInstanceExpansion(long begin, long end, String localTimezone,
1172                                           Cursor entries) {
1173         RecurrenceProcessor rp = new RecurrenceProcessor();
1174 
1175         // Key into the instance values to hold the original event concatenated with calendar id.
1176         final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR";
1177 
1178         int statusColumn = entries.getColumnIndex(Events.STATUS);
1179         int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
1180         int dtendColumn = entries.getColumnIndex(Events.DTEND);
1181         int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
1182         int durationColumn = entries.getColumnIndex(Events.DURATION);
1183         int rruleColumn = entries.getColumnIndex(Events.RRULE);
1184         int rdateColumn = entries.getColumnIndex(Events.RDATE);
1185         int exruleColumn = entries.getColumnIndex(Events.EXRULE);
1186         int exdateColumn = entries.getColumnIndex(Events.EXDATE);
1187         int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
1188         int idColumn = entries.getColumnIndex(Events._ID);
1189         int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
1190         int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
1191         int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
1192         int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID);
1193         int deletedColumn = entries.getColumnIndex(Events.DELETED);
1194 
1195         ContentValues initialValues;
1196         EventInstancesMap instancesMap = new EventInstancesMap();
1197 
1198         Duration duration = new Duration();
1199         Time eventTime = new Time();
1200 
1201         // Invariant: entries contains all events that affect the current
1202         // window.  It consists of:
1203         // a) Individual events that fall in the window.  These will be
1204         //    displayed.
1205         // b) Recurrences that included the window.  These will be displayed
1206         //    if not canceled.
1207         // c) Recurrence exceptions that fall in the window.  These will be
1208         //    displayed if not cancellations.
1209         // d) Recurrence exceptions that modify an instance inside the
1210         //    window (subject to 1 week assumption above), but are outside
1211         //    the window.  These will not be displayed.  Cases c and d are
1212         //    distingushed by the start / end time.
1213 
1214         while (entries.moveToNext()) {
1215             try {
1216                 initialValues = null;
1217 
1218                 boolean allDay = entries.getInt(allDayColumn) != 0;
1219 
1220                 String eventTimezone = entries.getString(eventTimezoneColumn);
1221                 if (allDay || TextUtils.isEmpty(eventTimezone)) {
1222                     // in the events table, allDay events start at midnight.
1223                     // this forces them to stay at midnight for all day events
1224                     // TODO: check that this actually does the right thing.
1225                     eventTimezone = Time.TIMEZONE_UTC;
1226                 }
1227 
1228                 long dtstartMillis = entries.getLong(dtstartColumn);
1229                 Long eventId = Long.valueOf(entries.getLong(idColumn));
1230 
1231                 String durationStr = entries.getString(durationColumn);
1232                 if (durationStr != null) {
1233                     try {
1234                         duration.parse(durationStr);
1235                     }
1236                     catch (DateException e) {
1237                         if (Log.isLoggable(TAG, Log.WARN)) {
1238                             Log.w(TAG, "error parsing duration for event "
1239                                     + eventId + "'" + durationStr + "'", e);
1240                         }
1241                         duration.sign = 1;
1242                         duration.weeks = 0;
1243                         duration.days = 0;
1244                         duration.hours = 0;
1245                         duration.minutes = 0;
1246                         duration.seconds = 0;
1247                         durationStr = "+P0S";
1248                     }
1249                 }
1250 
1251                 String syncId = entries.getString(syncIdColumn);
1252                 String originalEvent = entries.getString(originalEventColumn);
1253 
1254                 long originalInstanceTimeMillis = -1;
1255                 if (!entries.isNull(originalInstanceTimeColumn)) {
1256                     originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
1257                 }
1258                 int status = entries.getInt(statusColumn);
1259                 boolean deleted = (entries.getInt(deletedColumn) != 0);
1260 
1261                 String rruleStr = entries.getString(rruleColumn);
1262                 String rdateStr = entries.getString(rdateColumn);
1263                 String exruleStr = entries.getString(exruleColumn);
1264                 String exdateStr = entries.getString(exdateColumn);
1265                 long calendarId = entries.getLong(calendarIdColumn);
1266                 String syncIdKey = getSyncIdKey(syncId, calendarId); // key into instancesMap
1267 
1268                 RecurrenceSet recur = null;
1269                 try {
1270                     recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
1271                 } catch (EventRecurrence.InvalidFormatException e) {
1272                     if (Log.isLoggable(TAG, Log.WARN)) {
1273                         Log.w(TAG, "Could not parse RRULE recurrence string: " + rruleStr, e);
1274                     }
1275                     continue;
1276                 }
1277 
1278                 if (null != recur && recur.hasRecurrence()) {
1279                     // the event is repeating
1280 
1281                     if (status == Events.STATUS_CANCELED) {
1282                         // should not happen!
1283                         if (Log.isLoggable(TAG, Log.ERROR)) {
1284                             Log.e(TAG, "Found canceled recurring event in "
1285                                     + "Events table.  Ignoring.");
1286                         }
1287                         continue;
1288                     }
1289 
1290                     if (deleted) {
1291                         if (Log.isLoggable(TAG, Log.DEBUG)) {
1292                             Log.d(TAG, "Found deleted recurring event in "
1293                                     + "Events table.  Ignoring.");
1294                         }
1295                         continue;
1296                     }
1297 
1298                     // need to parse the event into a local calendar.
1299                     eventTime.timezone = eventTimezone;
1300                     eventTime.set(dtstartMillis);
1301                     eventTime.allDay = allDay;
1302 
1303                     if (durationStr == null) {
1304                         // should not happen.
1305                         if (Log.isLoggable(TAG, Log.ERROR)) {
1306                             Log.e(TAG, "Repeating event has no duration -- "
1307                                     + "should not happen.");
1308                         }
1309                         if (allDay) {
1310                             // set to one day.
1311                             duration.sign = 1;
1312                             duration.weeks = 0;
1313                             duration.days = 1;
1314                             duration.hours = 0;
1315                             duration.minutes = 0;
1316                             duration.seconds = 0;
1317                             durationStr = "+P1D";
1318                         } else {
1319                             // compute the duration from dtend, if we can.
1320                             // otherwise, use 0s.
1321                             duration.sign = 1;
1322                             duration.weeks = 0;
1323                             duration.days = 0;
1324                             duration.hours = 0;
1325                             duration.minutes = 0;
1326                             if (!entries.isNull(dtendColumn)) {
1327                                 long dtendMillis = entries.getLong(dtendColumn);
1328                                 duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
1329                                 durationStr = "+P" + duration.seconds + "S";
1330                             } else {
1331                                 duration.seconds = 0;
1332                                 durationStr = "+P0S";
1333                             }
1334                         }
1335                     }
1336 
1337                     long[] dates;
1338                     dates = rp.expand(eventTime, recur, begin, end);
1339 
1340                     // Initialize the "eventTime" timezone outside the loop.
1341                     // This is used in computeTimezoneDependentFields().
1342                     if (allDay) {
1343                         eventTime.timezone = Time.TIMEZONE_UTC;
1344                     } else {
1345                         eventTime.timezone = localTimezone;
1346                     }
1347 
1348                     long durationMillis = duration.getMillis();
1349                     for (long date : dates) {
1350                         initialValues = new ContentValues();
1351                         initialValues.put(Instances.EVENT_ID, eventId);
1352 
1353                         initialValues.put(Instances.BEGIN, date);
1354                         long dtendMillis = date + durationMillis;
1355                         initialValues.put(Instances.END, dtendMillis);
1356 
1357                         computeTimezoneDependentFields(date, dtendMillis,
1358                                 eventTime, initialValues);
1359                         instancesMap.add(syncIdKey, initialValues);
1360                     }
1361                 } else {
1362                     // the event is not repeating
1363                     initialValues = new ContentValues();
1364 
1365                     // if this event has an "original" field, then record
1366                     // that we need to cancel the original event (we can't
1367                     // do that here because the order of this loop isn't
1368                     // defined)
1369                     if (originalEvent != null && originalInstanceTimeMillis != -1) {
1370                         // The ORIGINAL_EVENT_AND_CALENDAR holds the
1371                         // calendar id concatenated with the ORIGINAL_EVENT to form
1372                         // a unique key, matching the keys for instancesMap.
1373                         initialValues.put(ORIGINAL_EVENT_AND_CALENDAR,
1374                                 getSyncIdKey(originalEvent, calendarId));
1375                         initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
1376                                 originalInstanceTimeMillis);
1377                         initialValues.put(Events.STATUS, status);
1378                     }
1379 
1380                     long dtendMillis = dtstartMillis;
1381                     if (durationStr == null) {
1382                         if (!entries.isNull(dtendColumn)) {
1383                             dtendMillis = entries.getLong(dtendColumn);
1384                         }
1385                     } else {
1386                         dtendMillis = duration.addTo(dtstartMillis);
1387                     }
1388 
1389                     // this non-recurring event might be a recurrence exception that doesn't
1390                     // actually fall within our expansion window, but instead was selected
1391                     // so we can correctly cancel expanded recurrence instances below.  do not
1392                     // add events to the instances map if they don't actually fall within our
1393                     // expansion window.
1394                     if ((dtendMillis < begin) || (dtstartMillis > end)) {
1395                         if (originalEvent != null && originalInstanceTimeMillis != -1) {
1396                             initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
1397                         } else {
1398                             if (Log.isLoggable(TAG, Log.WARN)) {
1399                                 Log.w(TAG, "Unexpected event outside window: " + syncId);
1400                             }
1401                             continue;
1402                         }
1403                     }
1404 
1405                     initialValues.put(Instances.EVENT_ID, eventId);
1406 
1407                     initialValues.put(Instances.BEGIN, dtstartMillis);
1408                     initialValues.put(Instances.END, dtendMillis);
1409 
1410                     // we temporarily store the DELETED status (will be cleaned later)
1411                     initialValues.put(Events.DELETED, deleted);
1412 
1413                     if (allDay) {
1414                         eventTime.timezone = Time.TIMEZONE_UTC;
1415                     } else {
1416                         eventTime.timezone = localTimezone;
1417                     }
1418                     computeTimezoneDependentFields(dtstartMillis, dtendMillis,
1419                             eventTime, initialValues);
1420 
1421                     instancesMap.add(syncIdKey, initialValues);
1422                 }
1423             } catch (DateException e) {
1424                 if (Log.isLoggable(TAG, Log.WARN)) {
1425                     Log.w(TAG, "RecurrenceProcessor error ", e);
1426                 }
1427             } catch (TimeFormatException e) {
1428                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
1429                     Log.w(TAG, "RecurrenceProcessor error ", e);
1430                 }
1431             }
1432         }
1433 
1434         // Invariant: instancesMap contains all instances that affect the
1435         // window, indexed by original sync id concatenated with calendar id.
1436         // It consists of:
1437         // a) Individual events that fall in the window.  They have:
1438         //   EVENT_ID, BEGIN, END
1439         // b) Instances of recurrences that fall in the window.  They may
1440         //   be subject to exceptions.  They have:
1441         //   EVENT_ID, BEGIN, END
1442         // c) Exceptions that fall in the window.  They have:
1443         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can
1444         //   be a modification or cancellation), EVENT_ID, BEGIN, END
1445         // d) Recurrence exceptions that modify an instance inside the
1446         //   window but fall outside the window.  They have:
1447         //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS =
1448         //   STATUS_CANCELED, EVENT_ID, BEGIN, END
1449 
1450         // First, delete the original instances corresponding to recurrence
1451         // exceptions.  We do this by iterating over the list and for each
1452         // recurrence exception, we search the list for an instance with a
1453         // matching "original instance time".  If we find such an instance,
1454         // we remove it from the list.  If we don't find such an instance
1455         // then we cancel the recurrence exception.
1456         Set<String> keys = instancesMap.keySet();
1457         for (String syncIdKey : keys) {
1458             InstancesList list = instancesMap.get(syncIdKey);
1459             for (ContentValues values : list) {
1460 
1461                 // If this instance is not a recurrence exception, then
1462                 // skip it.
1463                 if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) {
1464                     continue;
1465                 }
1466 
1467                 String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR);
1468                 long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
1469                 InstancesList originalList = instancesMap.get(originalEventPlusCalendar);
1470                 if (originalList == null) {
1471                     // The original recurrence is not present, so don't try canceling it.
1472                     continue;
1473                 }
1474 
1475                 // Search the original event for a matching original
1476                 // instance time.  If there is a matching one, then remove
1477                 // the original one.  We do this both for exceptions that
1478                 // change the original instance as well as for exceptions
1479                 // that delete the original instance.
1480                 for (int num = originalList.size() - 1; num >= 0; num--) {
1481                     ContentValues originalValues = originalList.get(num);
1482                     long beginTime = originalValues.getAsLong(Instances.BEGIN);
1483                     if (beginTime == originalTime) {
1484                         // We found the original instance, so remove it.
1485                         originalList.remove(num);
1486                     }
1487                 }
1488             }
1489         }
1490 
1491         // Invariant: instancesMap contains filtered instances.
1492         // It consists of:
1493         // a) Individual events that fall in the window.
1494         // b) Instances of recurrences that fall in the window and have not
1495         //   been subject to exceptions.
1496         // c) Exceptions that fall in the window.  They will have
1497         //   STATUS_CANCELED if they are cancellations.
1498         // d) Recurrence exceptions that modify an instance inside the
1499         //   window but fall outside the window.  These are STATUS_CANCELED.
1500 
1501         // Now do the inserts.  Since the db lock is held when this method is executed,
1502         // this will be done in a transaction.
1503         // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
1504         // while the calendar app is trying to query the db (expanding instances)), we will
1505         // not be "polite" and yield the lock until we're done.  This will favor local query
1506         // operations over sync/write operations.
1507         for (String syncIdKey : keys) {
1508             InstancesList list = instancesMap.get(syncIdKey);
1509             for (ContentValues values : list) {
1510 
1511                 // If this instance was cancelled or deleted then don't create a new
1512                 // instance.
1513                 Integer status = values.getAsInteger(Events.STATUS);
1514                 boolean deleted = values.containsKey(Events.DELETED) ?
1515                         values.getAsBoolean(Events.DELETED) : false;
1516                 if ((status != null && status == Events.STATUS_CANCELED) || deleted) {
1517                     continue;
1518                 }
1519 
1520                 // We remove this useless key (not valid in the context of Instances table)
1521                 values.remove(Events.DELETED);
1522 
1523                 // Remove these fields before inserting a new instance
1524                 values.remove(ORIGINAL_EVENT_AND_CALENDAR);
1525                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
1526                 values.remove(Events.STATUS);
1527 
1528                 mDbHelper.instancesReplace(values);
1529             }
1530         }
1531     }
1532 
1533     /**
1534      * Computes the timezone-dependent fields of an instance of an event and
1535      * updates the "values" map to contain those fields.
1536      *
1537      * @param begin the start time of the instance (in UTC milliseconds)
1538      * @param end the end time of the instance (in UTC milliseconds)
1539      * @param local a Time object with the timezone set to the local timezone
1540      * @param values a map that will contain the timezone-dependent fields
1541      */
computeTimezoneDependentFields(long begin, long end, Time local, ContentValues values)1542     private void computeTimezoneDependentFields(long begin, long end,
1543             Time local, ContentValues values) {
1544         local.set(begin);
1545         int startDay = Time.getJulianDay(begin, local.gmtoff);
1546         int startMinute = local.hour * 60 + local.minute;
1547 
1548         local.set(end);
1549         int endDay = Time.getJulianDay(end, local.gmtoff);
1550         int endMinute = local.hour * 60 + local.minute;
1551 
1552         // Special case for midnight, which has endMinute == 0.  Change
1553         // that to +24 hours on the previous day to make everything simpler.
1554         // Exception: if start and end minute are both 0 on the same day,
1555         // then leave endMinute alone.
1556         if (endMinute == 0 && endDay > startDay) {
1557             endMinute = 24 * 60;
1558             endDay -= 1;
1559         }
1560 
1561         values.put(Instances.START_DAY, startDay);
1562         values.put(Instances.END_DAY, endDay);
1563         values.put(Instances.START_MINUTE, startMinute);
1564         values.put(Instances.END_MINUTE, endMinute);
1565     }
1566 
1567     @Override
getType(Uri url)1568     public String getType(Uri url) {
1569         int match = sUriMatcher.match(url);
1570         switch (match) {
1571             case EVENTS:
1572                 return "vnd.android.cursor.dir/event";
1573             case EVENTS_ID:
1574                 return "vnd.android.cursor.item/event";
1575             case REMINDERS:
1576                 return "vnd.android.cursor.dir/reminder";
1577             case REMINDERS_ID:
1578                 return "vnd.android.cursor.item/reminder";
1579             case CALENDAR_ALERTS:
1580                 return "vnd.android.cursor.dir/calendar-alert";
1581             case CALENDAR_ALERTS_BY_INSTANCE:
1582                 return "vnd.android.cursor.dir/calendar-alert-by-instance";
1583             case CALENDAR_ALERTS_ID:
1584                 return "vnd.android.cursor.item/calendar-alert";
1585             case INSTANCES:
1586             case INSTANCES_BY_DAY:
1587             case EVENT_DAYS:
1588                 return "vnd.android.cursor.dir/event-instance";
1589             case TIME:
1590                 return "time/epoch";
1591             case PROVIDER_PROPERTIES:
1592                 return "vnd.android.cursor.dir/property";
1593             default:
1594                 throw new IllegalArgumentException("Unknown URL " + url);
1595         }
1596     }
1597 
isRecurrenceEvent(String rrule, String rdate, String originalEvent)1598     public static boolean isRecurrenceEvent(String rrule, String rdate, String originalEvent) {
1599         return (!TextUtils.isEmpty(rrule)||
1600                 !TextUtils.isEmpty(rdate)||
1601                 !TextUtils.isEmpty(originalEvent));
1602     }
1603 
1604     /**
1605      * Takes an event and corrects the hrs, mins, secs if it is an allDay event.
1606      *
1607      * AllDay events should have hrs, mins, secs set to zero. This checks if this is true and
1608      * corrects the fields DTSTART, DTEND, and DURATION if necessary. Also checks to ensure that
1609      * either both DTSTART and DTEND or DTSTART and DURATION are set for each event.
1610      *
1611      * @param updatedValues The values to check and correct
1612      * @return Returns true if a correction was necessary, false otherwise
1613      */
fixAllDayTime(Uri uri, ContentValues updatedValues)1614     private boolean fixAllDayTime(Uri uri, ContentValues updatedValues) {
1615         boolean neededCorrection = false;
1616         if (updatedValues.containsKey(Events.ALL_DAY)
1617                 && updatedValues.getAsInteger(Events.ALL_DAY).intValue() == 1) {
1618             Long dtstart = updatedValues.getAsLong(Events.DTSTART);
1619             Long dtend = updatedValues.getAsLong(Events.DTEND);
1620             String duration = updatedValues.getAsString(Events.DURATION);
1621             Time time = new Time();
1622             Cursor currentTimesCursor = null;
1623             String tempValue;
1624             // If a complete set of time fields doesn't exist query the db for them. A complete set
1625             // is dtstart and dtend for non-recurring events or dtstart and duration for recurring
1626             // events.
1627             if(dtstart == null || (dtend == null && duration == null)) {
1628                 // Make sure we have an id to search for, if not this is probably a new event
1629                 if (uri.getPathSegments().size() == 2) {
1630                     currentTimesCursor = query(uri,
1631                             ALLDAY_TIME_PROJECTION,
1632                             null /* selection */,
1633                             null /* selectionArgs */,
1634                             null /* sort */);
1635                     if (currentTimesCursor != null) {
1636                         if (!currentTimesCursor.moveToFirst() ||
1637                                 currentTimesCursor.getCount() != 1) {
1638                             // Either this is a new event or the query is too general to get data
1639                             // from the db. In either case don't try to use the query and catch
1640                             // errors when trying to update the time fields.
1641                             currentTimesCursor.close();
1642                             currentTimesCursor = null;
1643                         }
1644                     }
1645                 }
1646             }
1647 
1648             // Ensure dtstart exists for this event (always required) and set so h,m,s are 0 if
1649             // necessary.
1650             // TODO Move this somewhere to check all events, not just allDay events.
1651             if (dtstart == null) {
1652                 if (currentTimesCursor != null) {
1653                     // getLong returns 0 for empty fields, we'd like to know if a field is empty
1654                     // so getString is used instead.
1655                     tempValue = currentTimesCursor.getString(ALLDAY_DTSTART_INDEX);
1656                     try {
1657                         dtstart = Long.valueOf(tempValue);
1658                     } catch (NumberFormatException e) {
1659                         currentTimesCursor.close();
1660                         throw new IllegalArgumentException("Event has no DTSTART field, the db " +
1661                             "may be damaged. Set DTSTART for this event to fix.");
1662                     }
1663                 } else {
1664                     throw new IllegalArgumentException("DTSTART cannot be empty for new events.");
1665                 }
1666             }
1667             time.clear(Time.TIMEZONE_UTC);
1668             time.set(dtstart.longValue());
1669             if (time.hour != 0 || time.minute != 0 || time.second != 0) {
1670                 time.hour = 0;
1671                 time.minute = 0;
1672                 time.second = 0;
1673                 updatedValues.put(Events.DTSTART, time.toMillis(true));
1674                 neededCorrection = true;
1675             }
1676 
1677             // If dtend exists for this event make sure it's h,m,s are 0.
1678             if (dtend == null && currentTimesCursor != null) {
1679                 // getLong returns 0 for empty fields. We'd like to know if a field is empty
1680                 // so getString is used instead.
1681                 tempValue = currentTimesCursor.getString(ALLDAY_DTEND_INDEX);
1682                 try {
1683                     dtend = Long.valueOf(tempValue);
1684                 } catch (NumberFormatException e) {
1685                     dtend = null;
1686                 }
1687             }
1688             if (dtend != null) {
1689                 time.clear(Time.TIMEZONE_UTC);
1690                 time.set(dtend.longValue());
1691                 if (time.hour != 0 || time.minute != 0 || time.second != 0) {
1692                     time.hour = 0;
1693                     time.minute = 0;
1694                     time.second = 0;
1695                     dtend = time.toMillis(true);
1696                     updatedValues.put(Events.DTEND, dtend);
1697                     neededCorrection = true;
1698                 }
1699             }
1700 
1701             if (currentTimesCursor != null) {
1702                 if (duration == null) {
1703                     duration = currentTimesCursor.getString(ALLDAY_DURATION_INDEX);
1704                 }
1705                 currentTimesCursor.close();
1706             }
1707 
1708             if (duration != null) {
1709                 int len = duration.length();
1710                 /* duration is stored as either "P<seconds>S" or "P<days>D". This checks if it's
1711                  * in the seconds format, and if so converts it to days.
1712                  */
1713                 if (len == 0) {
1714                     duration = null;
1715                 } else if (duration.charAt(0) == 'P' &&
1716                         duration.charAt(len - 1) == 'S') {
1717                     int seconds = Integer.parseInt(duration.substring(1, len - 1));
1718                     int days = (seconds + DAY_IN_SECONDS - 1) / DAY_IN_SECONDS;
1719                     duration = "P" + days + "D";
1720                     updatedValues.put(Events.DURATION, duration);
1721                     neededCorrection = true;
1722                 } else if (duration.charAt(0) != 'P' ||
1723                         duration.charAt(len - 1) != 'D') {
1724                     throw new IllegalArgumentException("duration is not formatted correctly. " +
1725                             "Should be 'P<seconds>S' or 'P<days>D'.");
1726                 }
1727             }
1728 
1729             if (duration == null && dtend == null) {
1730                 throw new IllegalArgumentException("DTEND and DURATION cannot both be null for " +
1731                         "an event.");
1732             }
1733         }
1734         return neededCorrection;
1735     }
1736 
1737     @Override
insertInTransaction(Uri uri, ContentValues values)1738     protected Uri insertInTransaction(Uri uri, ContentValues values) {
1739         if (Log.isLoggable(TAG, Log.VERBOSE)) {
1740             Log.v(TAG, "insertInTransaction: " + uri);
1741         }
1742 
1743         final boolean callerIsSyncAdapter =
1744                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
1745 
1746         final int match = sUriMatcher.match(uri);
1747         long id = 0;
1748 
1749         switch (match) {
1750               case SYNCSTATE:
1751                 id = mDbHelper.getSyncState().insert(mDb, values);
1752                 break;
1753             case EVENTS:
1754                 if (!callerIsSyncAdapter) {
1755                     values.put(Events._SYNC_DIRTY, 1);
1756                 }
1757                 if (!values.containsKey(Events.DTSTART)) {
1758                     throw new RuntimeException("DTSTART field missing from event");
1759                 }
1760                 // TODO: do we really need to make a copy?
1761                 ContentValues updatedValues = new ContentValues(values);
1762                 validateEventData(updatedValues);
1763                 // updateLastDate must be after validation, to ensure proper last date computation
1764                 updatedValues = updateLastDate(updatedValues);
1765                 if (updatedValues == null) {
1766                     throw new RuntimeException("Could not insert event.");
1767                     // return null;
1768                 }
1769                 String owner = null;
1770                 if (updatedValues.containsKey(Events.CALENDAR_ID) &&
1771                         !updatedValues.containsKey(Events.ORGANIZER)) {
1772                     owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
1773                     // TODO: This isn't entirely correct.  If a guest is adding a recurrence
1774                     // exception to an event, the organizer should stay the original organizer.
1775                     // This value doesn't go to the server and it will get fixed on sync,
1776                     // so it shouldn't really matter.
1777                     if (owner != null) {
1778                         updatedValues.put(Events.ORGANIZER, owner);
1779                     }
1780                 }
1781                 if (fixAllDayTime(uri, updatedValues)) {
1782                     if (Log.isLoggable(TAG, Log.WARN)) {
1783                         Log.w(TAG, "insertInTransaction: " +
1784                                 "allDay is true but sec, min, hour were not 0.");
1785                     }
1786                 }
1787                 id = mDbHelper.eventsInsert(updatedValues);
1788                 if (id != -1) {
1789                     updateEventRawTimesLocked(id, updatedValues);
1790                     updateInstancesLocked(updatedValues, id, true /* new event */, mDb);
1791 
1792                     // If we inserted a new event that specified the self-attendee
1793                     // status, then we need to add an entry to the attendees table.
1794                     if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
1795                         int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS);
1796                         if (owner == null) {
1797                             owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
1798                         }
1799                         createAttendeeEntry(id, status, owner);
1800                     }
1801                     // if the Event Timezone is defined, store it as the original one in the
1802                     // ExtendedProperties table
1803                     if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) {
1804                         String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE);
1805 
1806                         ContentValues expropsValues = new ContentValues();
1807                         expropsValues.put(Calendar.ExtendedProperties.EVENT_ID, id);
1808                         expropsValues.put(Calendar.ExtendedProperties.NAME,
1809                                 EXT_PROP_ORIGINAL_TIMEZONE);
1810                         expropsValues.put(Calendar.ExtendedProperties.VALUE, originalTimezone);
1811 
1812                         // Insert the extended property
1813                         long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues);
1814                         if (exPropId == -1) {
1815                             if (Log.isLoggable(TAG, Log.ERROR)) {
1816                                 Log.e(TAG, "Cannot add the original Timezone in the "
1817                                         + "ExtendedProperties table for Event: " + id);
1818                             }
1819                         } else {
1820                             // Update the Event for saying it has some extended properties
1821                             ContentValues eventValues = new ContentValues();
1822                             eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1");
1823                             int result = mDb.update("Events", eventValues, "_id=?",
1824                                     new String[] {String.valueOf(id)});
1825                             if (result <= 0) {
1826                                 if (Log.isLoggable(TAG, Log.ERROR)) {
1827                                     Log.e(TAG, "Cannot update hasExtendedProperties column"
1828                                             + " for Event: " + id);
1829                                 }
1830                             }
1831                         }
1832                     }
1833                     triggerAppWidgetUpdate(id);
1834                 }
1835                 break;
1836             case CALENDARS:
1837                 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
1838                 if (syncEvents != null && syncEvents == 1) {
1839                     String accountName = values.getAsString(Calendars._SYNC_ACCOUNT);
1840                     String accountType = values.getAsString(
1841                             Calendars._SYNC_ACCOUNT_TYPE);
1842                     final Account account = new Account(accountName, accountType);
1843                     String calendarUrl = values.getAsString(Calendars.URL);
1844                     mDbHelper.scheduleSync(account, false /* two-way sync */, calendarUrl);
1845                 }
1846                 id = mDbHelper.calendarsInsert(values);
1847                 break;
1848             case ATTENDEES:
1849                 if (!values.containsKey(Attendees.EVENT_ID)) {
1850                     throw new IllegalArgumentException("Attendees values must "
1851                             + "contain an event_id");
1852                 }
1853                 id = mDbHelper.attendeesInsert(values);
1854                 if (!callerIsSyncAdapter) {
1855                     setEventDirty(values.getAsInteger(Attendees.EVENT_ID));
1856                 }
1857 
1858                 // Copy the attendee status value to the Events table.
1859                 updateEventAttendeeStatus(mDb, values);
1860                 break;
1861             case REMINDERS:
1862                 if (!values.containsKey(Reminders.EVENT_ID)) {
1863                     throw new IllegalArgumentException("Reminders values must "
1864                             + "contain an event_id");
1865                 }
1866                 id = mDbHelper.remindersInsert(values);
1867                 if (!callerIsSyncAdapter) {
1868                     setEventDirty(values.getAsInteger(Reminders.EVENT_ID));
1869                 }
1870 
1871                 // Schedule another event alarm, if necessary
1872                 if (Log.isLoggable(TAG, Log.DEBUG)) {
1873                     Log.d(TAG, "insertInternal() changing reminder");
1874                 }
1875                 scheduleNextAlarm(false /* do not remove alarms */);
1876                 break;
1877             case CALENDAR_ALERTS:
1878                 if (!values.containsKey(CalendarAlerts.EVENT_ID)) {
1879                     throw new IllegalArgumentException("CalendarAlerts values must "
1880                             + "contain an event_id");
1881                 }
1882                 id = mDbHelper.calendarAlertsInsert(values);
1883                 // Note: dirty bit is not set for Alerts because it is not synced.
1884                 // It is generated from Reminders, which is synced.
1885                 break;
1886             case EXTENDED_PROPERTIES:
1887                 if (!values.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
1888                     throw new IllegalArgumentException("ExtendedProperties values must "
1889                             + "contain an event_id");
1890                 }
1891                 id = mDbHelper.extendedPropertiesInsert(values);
1892                 if (!callerIsSyncAdapter) {
1893                     setEventDirty(values.getAsInteger(Calendar.ExtendedProperties.EVENT_ID));
1894                 }
1895                 break;
1896             case DELETED_EVENTS:
1897             case EVENTS_ID:
1898             case REMINDERS_ID:
1899             case CALENDAR_ALERTS_ID:
1900             case EXTENDED_PROPERTIES_ID:
1901             case INSTANCES:
1902             case INSTANCES_BY_DAY:
1903             case EVENT_DAYS:
1904             case PROVIDER_PROPERTIES:
1905                 throw new UnsupportedOperationException("Cannot insert into that URL: " + uri);
1906             default:
1907                 throw new IllegalArgumentException("Unknown URL " + uri);
1908         }
1909 
1910         if (id < 0) {
1911             return null;
1912         }
1913 
1914         return ContentUris.withAppendedId(uri, id);
1915     }
1916 
1917     /**
1918      * Do some validation on event data before inserting.
1919      * In particular make sure dtend, duration, etc make sense for
1920      * the type of event (regular, recurrence, exception).  Remove
1921      * any unexpected fields.
1922      *
1923      * @param values the ContentValues to insert
1924      */
validateEventData(ContentValues values)1925     private void validateEventData(ContentValues values) {
1926         boolean hasDtend = values.getAsLong(Events.DTEND) != null;
1927         boolean hasDuration = !TextUtils.isEmpty(values.getAsString(Events.DURATION));
1928         boolean hasRrule = !TextUtils.isEmpty(values.getAsString(Events.RRULE));
1929         boolean hasRdate = !TextUtils.isEmpty(values.getAsString(Events.RDATE));
1930         boolean hasOriginalEvent = !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT));
1931         boolean hasOriginalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME) != null;
1932         if (hasRrule || hasRdate) {
1933             // Recurrence:
1934             // dtstart is start time of first event
1935             // dtend is null
1936             // duration is the duration of the event
1937             // rrule is the recurrence rule
1938             // lastDate is the end of the last event or null if it repeats forever
1939             // originalEvent is null
1940             // originalInstanceTime is null
1941             if (hasDtend || !hasDuration || hasOriginalEvent || hasOriginalInstanceTime) {
1942                 if (Log.isLoggable(TAG, Log.DEBUG)) {
1943                     Log.e(TAG, "Invalid values for recurrence: " + values);
1944                 }
1945                 values.remove(Events.DTEND);
1946                 values.remove(Events.ORIGINAL_EVENT);
1947                 values.remove(Events.ORIGINAL_INSTANCE_TIME);
1948             }
1949         } else if (hasOriginalEvent || hasOriginalInstanceTime) {
1950             // Recurrence exception
1951             // dtstart is start time of exception event
1952             // dtend is end time of exception event
1953             // duration is null
1954             // rrule is null
1955             // lastdate is same as dtend
1956             // originalEvent is the _sync_id of the recurrence
1957             // originalInstanceTime is the start time of the event being replaced
1958             if (!hasDtend || hasDuration || !hasOriginalEvent || !hasOriginalInstanceTime) {
1959                 if (Log.isLoggable(TAG, Log.DEBUG)) {
1960                     Log.e(TAG, "Invalid values for recurrence exception: " + values);
1961                 }
1962                 values.remove(Events.DURATION);
1963             }
1964         } else {
1965             // Regular event
1966             // dtstart is the start time
1967             // dtend is the end time
1968             // duration is null
1969             // rrule is null
1970             // lastDate is the same as dtend
1971             // originalEvent is null
1972             // originalInstanceTime is null
1973             if (!hasDtend || hasDuration) {
1974                 if (Log.isLoggable(TAG, Log.DEBUG)) {
1975                     Log.e(TAG, "Invalid values for event: " + values);
1976                 }
1977                 values.remove(Events.DURATION);
1978             }
1979         }
1980     }
1981 
setEventDirty(int eventId)1982     private void setEventDirty(int eventId) {
1983         mDb.execSQL("UPDATE Events SET _sync_dirty=1 where _id=?", new Integer[] {eventId});
1984     }
1985 
1986     /**
1987      * Gets the calendar's owner for an event.
1988      * @param calId
1989      * @return email of owner or null
1990      */
getOwner(long calId)1991     private String getOwner(long calId) {
1992         if (calId < 0) {
1993             if (Log.isLoggable(TAG, Log.ERROR)) {
1994                 Log.e(TAG, "Calendar Id is not valid: " + calId);
1995             }
1996             return null;
1997         }
1998         // Get the email address of this user from this Calendar
1999         String emailAddress = null;
2000         Cursor cursor = null;
2001         try {
2002             cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2003                     new String[] { Calendars.OWNER_ACCOUNT },
2004                     null /* selection */,
2005                     null /* selectionArgs */,
2006                     null /* sort */);
2007             if (cursor == null || !cursor.moveToFirst()) {
2008                 if (Log.isLoggable(TAG, Log.DEBUG)) {
2009                     Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2010                 }
2011                 return null;
2012             }
2013             emailAddress = cursor.getString(0);
2014         } finally {
2015             if (cursor != null) {
2016                 cursor.close();
2017             }
2018         }
2019         return emailAddress;
2020     }
2021 
2022     /**
2023      * Creates an entry in the Attendees table that refers to the given event
2024      * and that has the given response status.
2025      *
2026      * @param eventId the event id that the new entry in the Attendees table
2027      * should refer to
2028      * @param status the response status
2029      * @param emailAddress the email of the attendee
2030      */
createAttendeeEntry(long eventId, int status, String emailAddress)2031     private void createAttendeeEntry(long eventId, int status, String emailAddress) {
2032         ContentValues values = new ContentValues();
2033         values.put(Attendees.EVENT_ID, eventId);
2034         values.put(Attendees.ATTENDEE_STATUS, status);
2035         values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
2036         // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
2037         // on sync.
2038         values.put(Attendees.ATTENDEE_RELATIONSHIP,
2039                 Attendees.RELATIONSHIP_ATTENDEE);
2040         values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
2041 
2042         // We don't know the ATTENDEE_NAME but that will be filled in by the
2043         // server and sent back to us.
2044         mDbHelper.attendeesInsert(values);
2045     }
2046 
2047     /**
2048      * Updates the attendee status in the Events table to be consistent with
2049      * the value in the Attendees table.
2050      *
2051      * @param db the database
2052      * @param attendeeValues the column values for one row in the Attendees
2053      * table.
2054      */
updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues)2055     private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
2056         // Get the event id for this attendee
2057         long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
2058 
2059         if (MULTIPLE_ATTENDEES_PER_EVENT) {
2060             // Get the calendar id for this event
2061             Cursor cursor = null;
2062             long calId;
2063             try {
2064                 cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
2065                         new String[] { Events.CALENDAR_ID },
2066                         null /* selection */,
2067                         null /* selectionArgs */,
2068                         null /* sort */);
2069                 if (cursor == null || !cursor.moveToFirst()) {
2070                     if (Log.isLoggable(TAG, Log.DEBUG)) {
2071                         Log.d(TAG, "Couldn't find " + eventId + " in Events table");
2072                     }
2073                     return;
2074                 }
2075                 calId = cursor.getLong(0);
2076             } finally {
2077                 if (cursor != null) {
2078                     cursor.close();
2079                 }
2080             }
2081 
2082             // Get the owner email for this Calendar
2083             String calendarEmail = null;
2084             cursor = null;
2085             try {
2086                 cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
2087                         new String[] { Calendars.OWNER_ACCOUNT },
2088                         null /* selection */,
2089                         null /* selectionArgs */,
2090                         null /* sort */);
2091                 if (cursor == null || !cursor.moveToFirst()) {
2092                     if (Log.isLoggable(TAG, Log.DEBUG)) {
2093                         Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
2094                     }
2095                     return;
2096                 }
2097                 calendarEmail = cursor.getString(0);
2098             } finally {
2099                 if (cursor != null) {
2100                     cursor.close();
2101                 }
2102             }
2103 
2104             if (calendarEmail == null) {
2105                 return;
2106             }
2107 
2108             // Get the email address for this attendee
2109             String attendeeEmail = null;
2110             if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
2111                 attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
2112             }
2113 
2114             // If the attendee email does not match the calendar email, then this
2115             // attendee is not the owner of this calendar so we don't update the
2116             // selfAttendeeStatus in the event.
2117             if (!calendarEmail.equals(attendeeEmail)) {
2118                 return;
2119             }
2120         }
2121 
2122         int status = Attendees.ATTENDEE_STATUS_NONE;
2123         if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
2124             int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
2125             if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
2126                 status = Attendees.ATTENDEE_STATUS_ACCEPTED;
2127             }
2128         }
2129 
2130         if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
2131             status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
2132         }
2133 
2134         ContentValues values = new ContentValues();
2135         values.put(Events.SELF_ATTENDEE_STATUS, status);
2136         db.update("Events", values, "_id=?", new String[] {String.valueOf(eventId)});
2137     }
2138 
2139     /**
2140      * Updates the instances table when an event is added or updated.
2141      * @param values The new values of the event.
2142      * @param rowId The database row id of the event.
2143      * @param newEvent true if the event is new.
2144      * @param db The database
2145      */
updateInstancesLocked(ContentValues values, long rowId, boolean newEvent, SQLiteDatabase db)2146     private void updateInstancesLocked(ContentValues values,
2147             long rowId,
2148             boolean newEvent,
2149             SQLiteDatabase db) {
2150 
2151         // If there are no expanded Instances, then return.
2152         MetaData.Fields fields = mMetaData.getFieldsLocked();
2153         if (fields.maxInstance == 0) {
2154             return;
2155         }
2156 
2157         Long dtstartMillis = values.getAsLong(Events.DTSTART);
2158         if (dtstartMillis == null) {
2159             if (newEvent) {
2160                 // must be present for a new event.
2161                 throw new RuntimeException("DTSTART missing.");
2162             }
2163             if (Log.isLoggable(TAG, Log.VERBOSE)) {
2164                 Log.v(TAG, "Missing DTSTART.  No need to update instance.");
2165             }
2166             return;
2167         }
2168 
2169         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
2170         Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
2171 
2172         if (!newEvent) {
2173             // Want to do this for regular event, recurrence, or exception.
2174             // For recurrence or exception, more deletion may happen below if we
2175             // do an instance expansion.  This deletion will suffice if the exception
2176             // is moved outside the window, for instance.
2177             db.delete("Instances", "event_id=?", new String[] {String.valueOf(rowId)});
2178         }
2179 
2180         String rrule = values.getAsString(Events.RRULE);
2181         String rdate = values.getAsString(Events.RDATE);
2182         String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
2183         if (isRecurrenceEvent(rrule, rdate, originalEvent))  {
2184             // The recurrence or exception needs to be (re-)expanded if:
2185             // a) Exception or recurrence that falls inside window
2186             boolean insideWindow = dtstartMillis <= fields.maxInstance &&
2187                     (lastDateMillis == null || lastDateMillis >= fields.minInstance);
2188             // b) Exception that affects instance inside window
2189             // These conditions match the query in getEntries
2190             //  See getEntries comment for explanation of subtracting 1 week.
2191             boolean affectsWindow = originalInstanceTime != null &&
2192                     originalInstanceTime <= fields.maxInstance &&
2193                     originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
2194             if (insideWindow || affectsWindow) {
2195                 updateRecurrenceInstancesLocked(values, rowId, db);
2196             }
2197             // TODO: an exception creation or update could be optimized by
2198             // updating just the affected instances, instead of regenerating
2199             // the recurrence.
2200             return;
2201         }
2202 
2203         Long dtendMillis = values.getAsLong(Events.DTEND);
2204         if (dtendMillis == null) {
2205             dtendMillis = dtstartMillis;
2206         }
2207 
2208         // if the event is in the expanded range, insert
2209         // into the instances table.
2210         // TODO: deal with durations.  currently, durations are only used in
2211         // recurrences.
2212 
2213         if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
2214             ContentValues instanceValues = new ContentValues();
2215             instanceValues.put(Instances.EVENT_ID, rowId);
2216             instanceValues.put(Instances.BEGIN, dtstartMillis);
2217             instanceValues.put(Instances.END, dtendMillis);
2218 
2219             boolean allDay = false;
2220             Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2221             if (allDayInteger != null) {
2222                 allDay = allDayInteger != 0;
2223             }
2224 
2225             // Update the timezone-dependent fields.
2226             Time local = new Time();
2227             if (allDay) {
2228                 local.timezone = Time.TIMEZONE_UTC;
2229             } else {
2230                 local.timezone = fields.timezone;
2231             }
2232 
2233             computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
2234             mDbHelper.instancesInsert(instanceValues);
2235         }
2236     }
2237 
2238     /**
2239      * Determines the recurrence entries associated with a particular recurrence.
2240      * This set is the base recurrence and any exception.
2241      *
2242      * Normally the entries are indicated by the sync id of the base recurrence
2243      * (which is the originalEvent in the exceptions).
2244      * However, a complication is that a recurrence may not yet have a sync id.
2245      * In that case, the recurrence is specified by the rowId.
2246      *
2247      * @param recurrenceSyncId The sync id of the base recurrence, or null.
2248      * @param rowId The row id of the base recurrence.
2249      * @return the relevant entries.
2250      */
getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId)2251     private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
2252         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
2253 
2254         qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
2255         qb.setProjectionMap(sEventsProjectionMap);
2256         String selectionArgs[];
2257         if (recurrenceSyncId == null) {
2258             String where = "_id =?";
2259             qb.appendWhere(where);
2260             selectionArgs = new String[] {String.valueOf(rowId)};
2261         } else {
2262             String where = "_sync_id = ? OR originalEvent = ?";
2263             qb.appendWhere(where);
2264             selectionArgs = new String[] {recurrenceSyncId, recurrenceSyncId};
2265         }
2266         if (Log.isLoggable(TAG, Log.VERBOSE)) {
2267             Log.v(TAG, "Retrieving events to expand: " + qb.toString());
2268         }
2269 
2270         return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs,
2271                 null /* groupBy */, null /* having */, null /* sortOrder */);
2272     }
2273 
2274     /**
2275      * Do incremental Instances update of a recurrence or recurrence exception.
2276      *
2277      * This method does performInstanceExpansion on just the modified recurrence,
2278      * to avoid the overhead of recomputing the entire instance table.
2279      *
2280      * @param values The new values of the event.
2281      * @param rowId The database row id of the event.
2282      * @param db The database
2283      */
updateRecurrenceInstancesLocked(ContentValues values, long rowId, SQLiteDatabase db)2284     private void updateRecurrenceInstancesLocked(ContentValues values,
2285             long rowId,
2286             SQLiteDatabase db) {
2287         MetaData.Fields fields = mMetaData.getFieldsLocked();
2288         String instancesTimezone = mCalendarCache.readTimezoneInstances();
2289         String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
2290         String recurrenceSyncId;
2291         if (originalEvent != null) {
2292             recurrenceSyncId = originalEvent;
2293         } else {
2294             // Get the recurrence's sync id from the database
2295             recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
2296                     + " WHERE _id=?", new String[] {String.valueOf(rowId)});
2297         }
2298         // recurrenceSyncId is the _sync_id of the underlying recurrence
2299         // If the recurrence hasn't gone to the server, it will be null.
2300 
2301         // Need to clear out old instances
2302         if (recurrenceSyncId == null) {
2303             // Creating updating a recurrence that hasn't gone to the server.
2304             // Need to delete based on row id
2305             String where = "_id IN (SELECT Instances._id as _id"
2306                     + " FROM Instances INNER JOIN Events"
2307                     + " ON (Events._id = Instances.event_id)"
2308                     + " WHERE Events._id =?)";
2309             db.delete("Instances", where, new String[]{"" + rowId});
2310         } else {
2311             // Creating or modifying a recurrence or exception.
2312             // Delete instances for recurrence (_sync_id = recurrenceSyncId)
2313             // and all exceptions (originalEvent = recurrenceSyncId)
2314             String where = "_id IN (SELECT Instances._id as _id"
2315                     + " FROM Instances INNER JOIN Events"
2316                     + " ON (Events._id = Instances.event_id)"
2317                     + " WHERE Events._sync_id =?"
2318                     + " OR Events.originalEvent =?)";
2319             db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
2320         }
2321 
2322         // Now do instance expansion
2323         Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
2324         try {
2325             performInstanceExpansion(fields.minInstance, fields.maxInstance, instancesTimezone,
2326                                      entries);
2327         } finally {
2328             if (entries != null) {
2329                 entries.close();
2330             }
2331         }
2332     }
2333 
calculateLastDate(ContentValues values)2334     long calculateLastDate(ContentValues values)
2335             throws DateException {
2336         // Allow updates to some event fields like the title or hasAlarm
2337         // without requiring DTSTART.
2338         if (!values.containsKey(Events.DTSTART)) {
2339             if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
2340                     || values.containsKey(Events.DURATION)
2341                     || values.containsKey(Events.EVENT_TIMEZONE)
2342                     || values.containsKey(Events.RDATE)
2343                     || values.containsKey(Events.EXRULE)
2344                     || values.containsKey(Events.EXDATE)) {
2345                 throw new RuntimeException("DTSTART field missing from event");
2346             }
2347             return -1;
2348         }
2349         long dtstartMillis = values.getAsLong(Events.DTSTART);
2350         long lastMillis = -1;
2351 
2352         // Can we use dtend with a repeating event?  What does that even
2353         // mean?
2354         // NOTE: if the repeating event has a dtend, we convert it to a
2355         // duration during event processing, so this situation should not
2356         // occur.
2357         Long dtEnd = values.getAsLong(Events.DTEND);
2358         if (dtEnd != null) {
2359             lastMillis = dtEnd;
2360         } else {
2361             // find out how long it is
2362             Duration duration = new Duration();
2363             String durationStr = values.getAsString(Events.DURATION);
2364             if (durationStr != null) {
2365                 duration.parse(durationStr);
2366             }
2367 
2368             RecurrenceSet recur = null;
2369             try {
2370                 recur = new RecurrenceSet(values);
2371             } catch (EventRecurrence.InvalidFormatException e) {
2372                 if (Log.isLoggable(TAG, Log.WARN)) {
2373                     Log.w(TAG, "Could not parse RRULE recurrence string: " +
2374                             values.get(Calendar.Events.RRULE), e);
2375                 }
2376                 return lastMillis; // -1
2377             }
2378 
2379             if (null != recur && recur.hasRecurrence()) {
2380                 // the event is repeating, so find the last date it
2381                 // could appear on
2382 
2383                 String tz = values.getAsString(Events.EVENT_TIMEZONE);
2384 
2385                 if (TextUtils.isEmpty(tz)) {
2386                     // floating timezone
2387                     tz = Time.TIMEZONE_UTC;
2388                 }
2389                 Time dtstartLocal = new Time(tz);
2390 
2391                 dtstartLocal.set(dtstartMillis);
2392 
2393                 RecurrenceProcessor rp = new RecurrenceProcessor();
2394                 lastMillis = rp.getLastOccurence(dtstartLocal, recur);
2395                 if (lastMillis == -1) {
2396                     return lastMillis;  // -1
2397                 }
2398             } else {
2399                 // the event is not repeating, just use dtstartMillis
2400                 lastMillis = dtstartMillis;
2401             }
2402 
2403             // that was the beginning of the event.  this is the end.
2404             lastMillis = duration.addTo(lastMillis);
2405         }
2406         return lastMillis;
2407     }
2408 
2409     /**
2410      * Add LAST_DATE to values.
2411      * @param values the ContentValues (in/out)
2412      * @return values on success, null on failure
2413      */
updateLastDate(ContentValues values)2414     private ContentValues updateLastDate(ContentValues values) {
2415         try {
2416             long last = calculateLastDate(values);
2417             if (last != -1) {
2418                 values.put(Events.LAST_DATE, last);
2419             }
2420 
2421             return values;
2422         } catch (DateException e) {
2423             // don't add it if there was an error
2424             if (Log.isLoggable(TAG, Log.WARN)) {
2425                 Log.w(TAG, "Could not calculate last date.", e);
2426             }
2427             return null;
2428         }
2429     }
2430 
updateEventRawTimesLocked(long eventId, ContentValues values)2431     private void updateEventRawTimesLocked(long eventId, ContentValues values) {
2432         ContentValues rawValues = new ContentValues();
2433 
2434         rawValues.put("event_id", eventId);
2435 
2436         String timezone = values.getAsString(Events.EVENT_TIMEZONE);
2437 
2438         boolean allDay = false;
2439         Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
2440         if (allDayInteger != null) {
2441             allDay = allDayInteger != 0;
2442         }
2443 
2444         if (allDay || TextUtils.isEmpty(timezone)) {
2445             // floating timezone
2446             timezone = Time.TIMEZONE_UTC;
2447         }
2448 
2449         Time time = new Time(timezone);
2450         time.allDay = allDay;
2451         Long dtstartMillis = values.getAsLong(Events.DTSTART);
2452         if (dtstartMillis != null) {
2453             time.set(dtstartMillis);
2454             rawValues.put("dtstart2445", time.format2445());
2455         }
2456 
2457         Long dtendMillis = values.getAsLong(Events.DTEND);
2458         if (dtendMillis != null) {
2459             time.set(dtendMillis);
2460             rawValues.put("dtend2445", time.format2445());
2461         }
2462 
2463         Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
2464         if (originalInstanceMillis != null) {
2465             // This is a recurrence exception so we need to get the all-day
2466             // status of the original recurring event in order to format the
2467             // date correctly.
2468             allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
2469             if (allDayInteger != null) {
2470                 time.allDay = allDayInteger != 0;
2471             }
2472             time.set(originalInstanceMillis);
2473             rawValues.put("originalInstanceTime2445", time.format2445());
2474         }
2475 
2476         Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
2477         if (lastDateMillis != null) {
2478             time.allDay = allDay;
2479             time.set(lastDateMillis);
2480             rawValues.put("lastDate2445", time.format2445());
2481         }
2482 
2483         mDbHelper.eventsRawTimesReplace(rawValues);
2484     }
2485 
2486     @Override
deleteInTransaction(Uri uri, String selection, String[] selectionArgs)2487     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
2488         if (Log.isLoggable(TAG, Log.VERBOSE)) {
2489             Log.v(TAG, "deleteInTransaction: " + uri);
2490         }
2491         final boolean callerIsSyncAdapter =
2492                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
2493         final int match = sUriMatcher.match(uri);
2494         switch (match) {
2495             case SYNCSTATE:
2496                 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
2497 
2498             case SYNCSTATE_ID:
2499                 String selectionWithId = (BaseColumns._ID + "=?")
2500                         + (selection == null ? "" : " AND (" + selection + ")");
2501                 // Prepend id to selectionArgs
2502                 selectionArgs = insertSelectionArg(selectionArgs,
2503                         String.valueOf(ContentUris.parseId(uri)));
2504                 return mDbHelper.getSyncState().delete(mDb, selectionWithId,
2505                         selectionArgs);
2506 
2507             case EVENTS:
2508             {
2509                 int result = 0;
2510                 selection = appendAccountToSelection(uri, selection);
2511 
2512                 // Query this event to get the ids to delete.
2513                 Cursor cursor = mDb.query("Events", ID_ONLY_PROJECTION,
2514                         selection, selectionArgs, null /* groupBy */,
2515                         null /* having */, null /* sortOrder */);
2516                 try {
2517                     while (cursor.moveToNext()) {
2518                         long id = cursor.getLong(0);
2519                         result += deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
2520                     }
2521                     scheduleNextAlarm(false /* do not remove alarms */);
2522                     triggerAppWidgetUpdate(-1 /* changedEventId */);
2523                 } finally {
2524                     cursor.close();
2525                     cursor = null;
2526                 }
2527                 return result;
2528             }
2529             case EVENTS_ID:
2530             {
2531                 long id = ContentUris.parseId(uri);
2532                 if (selection != null) {
2533                     throw new UnsupportedOperationException("CalendarProvider2 "
2534                             + "doesn't support selection based deletion for type "
2535                             + match);
2536                 }
2537                 return deleteEventInternal(id, callerIsSyncAdapter, false /* isBatch */);
2538             }
2539             case ATTENDEES:
2540             {
2541                 if (callerIsSyncAdapter) {
2542                     return mDb.delete("Attendees", selection, selectionArgs);
2543                 } else {
2544                     return deleteFromTable("Attendees", uri, selection, selectionArgs);
2545                 }
2546             }
2547             case ATTENDEES_ID:
2548             {
2549                 if (selection != null) {
2550                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
2551                 }
2552                 if (callerIsSyncAdapter) {
2553                     long id = ContentUris.parseId(uri);
2554                     return mDb.delete("Attendees", "_id=?", new String[] {String.valueOf(id)});
2555                 } else {
2556                     return deleteFromTable("Attendees", uri, null /* selection */,
2557                                            null /* selectionArgs */);
2558                 }
2559             }
2560             case REMINDERS:
2561             {
2562                 if (callerIsSyncAdapter) {
2563                     return mDb.delete("Reminders", selection, selectionArgs);
2564                 } else {
2565                     return deleteFromTable("Reminders", uri, selection, selectionArgs);
2566                 }
2567             }
2568             case REMINDERS_ID:
2569             {
2570                 if (selection != null) {
2571                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
2572                 }
2573                 if (callerIsSyncAdapter) {
2574                     long id = ContentUris.parseId(uri);
2575                     return mDb.delete("Reminders", "_id=?", new String[] {String.valueOf(id)});
2576                 } else {
2577                     return deleteFromTable("Reminders", uri, null /* selection */,
2578                                            null /* selectionArgs */);
2579                 }
2580             }
2581             case EXTENDED_PROPERTIES:
2582             {
2583                 if (callerIsSyncAdapter) {
2584                     return mDb.delete("ExtendedProperties", selection, selectionArgs);
2585                 } else {
2586                     return deleteFromTable("ExtendedProperties", uri, selection, selectionArgs);
2587                 }
2588             }
2589             case EXTENDED_PROPERTIES_ID:
2590             {
2591                 if (selection != null) {
2592                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
2593                 }
2594                 if (callerIsSyncAdapter) {
2595                     long id = ContentUris.parseId(uri);
2596                     return mDb.delete("ExtendedProperties", "_id=?",
2597                             new String[] {String.valueOf(id)});
2598                 } else {
2599                     return deleteFromTable("ExtendedProperties", uri, null /* selection */,
2600                                            null /* selectionArgs */);
2601                 }
2602             }
2603             case CALENDAR_ALERTS:
2604             {
2605                 if (callerIsSyncAdapter) {
2606                     return mDb.delete("CalendarAlerts", selection, selectionArgs);
2607                 } else {
2608                     return deleteFromTable("CalendarAlerts", uri, selection, selectionArgs);
2609                 }
2610             }
2611             case CALENDAR_ALERTS_ID:
2612             {
2613                 if (selection != null) {
2614                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
2615                 }
2616                 // Note: dirty bit is not set for Alerts because it is not synced.
2617                 // It is generated from Reminders, which is synced.
2618                 long id = ContentUris.parseId(uri);
2619                 return mDb.delete("CalendarAlerts", "_id=?", new String[] {String.valueOf(id)});
2620             }
2621             case DELETED_EVENTS:
2622                 throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
2623             case CALENDARS_ID:
2624                 StringBuilder selectionSb = new StringBuilder("_id=");
2625                 selectionSb.append(uri.getPathSegments().get(1));
2626                 if (!TextUtils.isEmpty(selection)) {
2627                     selectionSb.append(" AND (");
2628                     selectionSb.append(selection);
2629                     selectionSb.append(')');
2630                 }
2631                 selection = selectionSb.toString();
2632                 // fall through to CALENDARS for the actual delete
2633             case CALENDARS:
2634                 selection = appendAccountToSelection(uri, selection);
2635                 return deleteMatchingCalendars(selection); // TODO: handle in sync adapter
2636             case INSTANCES:
2637             case INSTANCES_BY_DAY:
2638             case EVENT_DAYS:
2639             case PROVIDER_PROPERTIES:
2640                 throw new UnsupportedOperationException("Cannot delete that URL");
2641             default:
2642                 throw new IllegalArgumentException("Unknown URL " + uri);
2643         }
2644     }
2645 
deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch)2646     private int deleteEventInternal(long id, boolean callerIsSyncAdapter, boolean isBatch) {
2647         int result = 0;
2648         String selectionArgs[] = new String[] {String.valueOf(id)};
2649 
2650         // Query this event to get the fields needed for deleting.
2651         Cursor cursor = mDb.query("Events", EVENTS_PROJECTION,
2652                 "_id=?", selectionArgs,
2653                 null /* groupBy */,
2654                 null /* having */, null /* sortOrder */);
2655         try {
2656             if (cursor.moveToNext()) {
2657                 result = 1;
2658                 String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
2659                 boolean emptySyncId = TextUtils.isEmpty(syncId);
2660 
2661                 // If this was a recurring event or a recurrence
2662                 // exception, then force a recalculation of the
2663                 // instances.
2664                 String rrule = cursor.getString(EVENTS_RRULE_INDEX);
2665                 String rdate = cursor.getString(EVENTS_RDATE_INDEX);
2666                 String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
2667                 if (isRecurrenceEvent(rrule, rdate, origEvent)) {
2668                     mMetaData.clearInstanceRange();
2669                 }
2670 
2671                 // we clean the Events and Attendees table if the caller is CalendarSyncAdapter
2672                 // or if the event is local (no syncId)
2673                 if (callerIsSyncAdapter || emptySyncId) {
2674                     mDb.delete("Events", "_id=?", selectionArgs);
2675                 } else {
2676                     ContentValues values = new ContentValues();
2677                     values.put(Events.DELETED, 1);
2678                     values.put(Events._SYNC_DIRTY, 1);
2679                     mDb.update("Events", values, "_id=?", selectionArgs);
2680 
2681                     // Delete associated data; attendees, however, are deleted with the actual event
2682                     // so that the sync adapter is able to notify attendees of the cancellation.
2683                     mDb.delete("Instances", "event_id=?", selectionArgs);
2684                     mDb.delete("EventsRawTimes", "event_id=?", selectionArgs);
2685                     mDb.delete("Reminders", "event_id=?", selectionArgs);
2686                     mDb.delete("CalendarAlerts", "event_id=?", selectionArgs);
2687                     mDb.delete("ExtendedProperties", "event_id=?", selectionArgs);
2688                 }
2689             }
2690         } finally {
2691             cursor.close();
2692             cursor = null;
2693         }
2694 
2695         if (!isBatch) {
2696             scheduleNextAlarm(false /* do not remove alarms */);
2697             triggerAppWidgetUpdate(-1 /* changedEventId */);
2698         }
2699 
2700         return result;
2701     }
2702 
2703     /**
2704      * Delete rows from a table and mark corresponding events as dirty.
2705      * @param table The table to delete from
2706      * @param uri The URI specifying the rows
2707      * @param selection for the query
2708      * @param selectionArgs for the query
2709      */
deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs)2710     private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) {
2711         // Note that the query will return data according to the access restrictions,
2712         // so we don't need to worry about deleting data we don't have permission to read.
2713         Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
2714         ContentValues values = new ContentValues();
2715         values.put(Events._SYNC_DIRTY, "1");
2716         int count = 0;
2717         try {
2718             while(c.moveToNext()) {
2719                 long id = c.getLong(ID_INDEX);
2720                 long event_id = c.getLong(EVENT_ID_INDEX);
2721                 mDb.delete(table, "_id=?", new String[] {String.valueOf(id)});
2722                 mDb.update("Events", values, "_id=?", new String[] {String.valueOf(event_id)});
2723                 count++;
2724             }
2725         } finally {
2726             c.close();
2727         }
2728         return count;
2729     }
2730 
2731     /**
2732      * Update rows in a table and mark corresponding events as dirty.
2733      * @param table The table to delete from
2734      * @param values The values to update
2735      * @param uri The URI specifying the rows
2736      * @param selection for the query
2737      * @param selectionArgs for the query
2738      */
updateInTable(String table, ContentValues values, Uri uri, String selection, String[] selectionArgs)2739     private int updateInTable(String table, ContentValues values, Uri uri, String selection,
2740             String[] selectionArgs) {
2741         // Note that the query will return data according to the access restrictions,
2742         // so we don't need to worry about deleting data we don't have permission to read.
2743         Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
2744         ContentValues dirtyValues = new ContentValues();
2745         dirtyValues.put(Events._SYNC_DIRTY, "1");
2746         int count = 0;
2747         try {
2748             while(c.moveToNext()) {
2749                 long id = c.getLong(ID_INDEX);
2750                 long event_id = c.getLong(EVENT_ID_INDEX);
2751                 mDb.update(table, values, "_id=?", new String[] {String.valueOf(id)});
2752                 mDb.update("Events", dirtyValues, "_id=?", new String[] {String.valueOf(event_id)});
2753                 count++;
2754             }
2755         } finally {
2756             c.close();
2757         }
2758         return count;
2759     }
2760 
deleteMatchingCalendars(String where)2761     private int deleteMatchingCalendars(String where) {
2762         // query to find all the calendars that match, for each
2763         // - delete calendar subscription
2764         // - delete calendar
2765 
2766         Cursor c = mDb.query("Calendars", sCalendarsIdProjection, where,
2767                 null /* selectionArgs */, null /* groupBy */,
2768                 null /* having */, null /* sortOrder */);
2769         if (c == null) {
2770             return 0;
2771         }
2772         try {
2773             while (c.moveToNext()) {
2774                 long id = c.getLong(CALENDARS_INDEX_ID);
2775                 modifyCalendarSubscription(id, false /* not selected */);
2776             }
2777         } finally {
2778             c.close();
2779         }
2780         return mDb.delete("Calendars", where, null /* whereArgs */);
2781     }
2782 
getCursorForEventIdAndProjection(String eventId, String[] projection)2783     private Cursor getCursorForEventIdAndProjection(String eventId, String[] projection) {
2784         return mDb.query(Tables.EVENTS,
2785                 projection,
2786                 SQL_WHERE_ID,
2787                 new String[] { eventId },
2788                 null /* group by */,
2789                 null /* having */,
2790                 null /* order by*/);
2791     }
2792 
doesEventExistForSyncId(String syncId)2793     private boolean doesEventExistForSyncId(String syncId) {
2794         if (syncId == null) {
2795             if (Log.isLoggable(TAG, Log.WARN)) {
2796                 Log.w(TAG, "SyncID cannot be null: " + syncId);
2797             }
2798             return false;
2799         }
2800         long count = DatabaseUtils.longForQuery(mDb, SQL_SELECT_COUNT_FOR_SYNC_ID,
2801                 new String[] { syncId });
2802         return (count > 0);
2803     }
2804 
2805     // Check if an UPDATE with STATUS_CANCEL means that we will need to do an Update (instead of
2806     // a Deletion)
2807     //
2808     // Deletion will be done only and only if:
2809     // - event status = canceled
2810     // - event is a recurrence exception that does not have its original (parent) event anymore
2811     //
2812     // This is due to the Server semantics that generate STATUS_CANCELED for both creation
2813     // and deletion of a recurrence exception
2814     // See bug #3218104
doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values)2815     private boolean doesStatusCancelUpdateMeanUpdate(String eventId, ContentValues values) {
2816         boolean isStatusCanceled = values.containsKey(Events.STATUS) &&
2817                 (values.getAsInteger(Events.STATUS) == Events.STATUS_CANCELED);
2818         if (isStatusCanceled) {
2819             Cursor cursor = null;
2820             try {
2821                 cursor = getCursorForEventIdAndProjection(eventId,
2822                         new String[] { Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT });
2823                 if (!cursor.moveToFirst()) {
2824                     if (Log.isLoggable(TAG, Log.WARN)) {
2825                         Log.w(TAG, "Cannot find Event with id: " + eventId);
2826                     }
2827                     return false;
2828                 }
2829                 String rrule = cursor.getString(0);
2830                 String rdate = cursor.getString(1);
2831                 String originalEvent = cursor.getString(2);
2832 
2833                 boolean isRecurrenceException =
2834                         isRecurrenceEvent(rrule, rdate, originalEvent) &&
2835                         !TextUtils.isEmpty(originalEvent);
2836 
2837                 if (isRecurrenceException) {
2838                     return doesEventExistForSyncId(originalEvent);
2839                 }
2840             } finally {
2841                 cursor.close();
2842             }
2843         }
2844         // This is the normal case, we just want an UPDATE
2845         return true;
2846     }
2847 
2848     // TODO: call calculateLastDate()!
2849     @Override
updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)2850     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
2851             String[] selectionArgs) {
2852         if (Log.isLoggable(TAG, Log.VERBOSE)) {
2853             Log.v(TAG, "updateInTransaction: " + uri);
2854         }
2855 
2856         int count = 0;
2857 
2858         final int match = sUriMatcher.match(uri);
2859 
2860         final boolean callerIsSyncAdapter =
2861                 readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
2862 
2863         // TODO: remove this restriction
2864         if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS
2865                 && match != EVENTS && match != PROVIDER_PROPERTIES) {
2866             throw new IllegalArgumentException(
2867                     "WHERE based updates not supported");
2868         }
2869         switch (match) {
2870             case SYNCSTATE:
2871                 return mDbHelper.getSyncState().update(mDb, values,
2872                         appendAccountToSelection(uri, selection), selectionArgs);
2873 
2874             case SYNCSTATE_ID: {
2875                 selection = appendAccountToSelection(uri, selection);
2876                 String selectionWithId = (BaseColumns._ID + "=?")
2877                         + (selection == null ? "" : " AND (" + selection + ")");
2878                 // Prepend id to selectionArgs
2879                 selectionArgs = insertSelectionArg(selectionArgs,
2880                         String.valueOf(ContentUris.parseId(uri)));
2881                 return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs);
2882             }
2883 
2884             case CALENDARS_ID:
2885             {
2886                 if (selection != null) {
2887                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
2888                 }
2889                 long id = ContentUris.parseId(uri);
2890                 Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
2891                 if (syncEvents != null) {
2892                     modifyCalendarSubscription(id, syncEvents == 1);
2893                 }
2894 
2895                 int result = mDb.update("Calendars", values, "_id=?",
2896                         new String[] {String.valueOf(id)});
2897 
2898                 // The calendar should not be displayed in widget either.
2899                 final Integer selected = values.getAsInteger(Calendars.SELECTED);
2900                 if (selected != null && selected == 0) {
2901                     triggerAppWidgetUpdate(-1);
2902                 }
2903 
2904                 return result;
2905             }
2906             case EVENTS:
2907             case EVENTS_ID:
2908             {
2909                 long id = 0;
2910                 if (match == EVENTS_ID) {
2911                     id = ContentUris.parseId(uri);
2912                 } else if (callerIsSyncAdapter) {
2913                     if (selection != null && selection.startsWith("_id=")) {
2914                         // The ContentProviderOperation generates an _id=n string instead of
2915                         // adding the id to the URL, so parse that out here.
2916                         id = Long.parseLong(selection.substring(4));
2917                     } else {
2918                         // Sync adapter Events operation affects just Events table, not associated
2919                         // tables.
2920                         if (fixAllDayTime(uri, values)) {
2921                             if (Log.isLoggable(TAG, Log.WARN)) {
2922                                 Log.w(TAG, "updateInTransaction: Caller is sync adapter. " +
2923                                         "allDay is true but sec, min, hour were not 0.");
2924                             }
2925                         }
2926                         return mDb.update("Events", values, selection, selectionArgs);
2927                     }
2928                 } else {
2929                     throw new IllegalArgumentException("Unknown URL " + uri);
2930                 }
2931                 if (!callerIsSyncAdapter) {
2932                     values.put(Events._SYNC_DIRTY, 1);
2933                 }
2934                 // Disallow updating the attendee status in the Events
2935                 // table.  In the future, we could support this but we
2936                 // would have to query and update the attendees table
2937                 // to keep the values consistent.
2938                 if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
2939                     throw new IllegalArgumentException("Updating "
2940                             + Events.SELF_ATTENDEE_STATUS
2941                             + " in Events table is not allowed.");
2942                 }
2943 
2944                 // TODO: should we allow this?
2945                 if (values.containsKey(Events.HTML_URI) && !callerIsSyncAdapter) {
2946                     throw new IllegalArgumentException("Updating "
2947                             + Events.HTML_URI
2948                             + " in Events table is not allowed.");
2949                 }
2950                 String strId = String.valueOf(id);
2951                 // For taking care about recurrences exceptions cancelations, check if this needs
2952                 //  to be an UPDATE or a DELETE
2953                 boolean isUpdate = doesStatusCancelUpdateMeanUpdate(strId, values);
2954                 ContentValues updatedValues = new ContentValues(values);
2955                 // TODO: should extend validateEventData to work with updates and call it here
2956                 updatedValues = updateLastDate(updatedValues);
2957                 if (updatedValues == null) {
2958                     if (Log.isLoggable(TAG, Log.WARN)) {
2959                         Log.w(TAG, "Could not update event.");
2960                     }
2961                     return 0;
2962                 }
2963                 // Make sure we pass in a uri with the id appended to fixAllDayTime
2964                 Uri allDayUri;
2965                 if (uri.getPathSegments().size() == 1) {
2966                     allDayUri = ContentUris.withAppendedId(uri, id);
2967                 } else {
2968                     allDayUri = uri;
2969                 }
2970                 if (fixAllDayTime(allDayUri, updatedValues)) {
2971                     if (Log.isLoggable(TAG, Log.WARN)) {
2972                         Log.w(TAG, "updateInTransaction: " +
2973                                 "allDay is true but sec, min, hour were not 0.");
2974                     }
2975                 }
2976 
2977                 int result;
2978 
2979                 if (isUpdate) {
2980                     result = mDb.update("Events", updatedValues, "_id=?",
2981                             new String[] {String.valueOf(id)});
2982                     if (result > 0) {
2983                         updateEventRawTimesLocked(id, updatedValues);
2984                         updateInstancesLocked(updatedValues, id, false /* not a new event */, mDb);
2985 
2986                         if (values.containsKey(Events.DTSTART)) {
2987                             // The start time of the event changed, so run the
2988                             // event alarm scheduler.
2989                             if (Log.isLoggable(TAG, Log.DEBUG)) {
2990                                 Log.d(TAG, "updateInternal() changing event");
2991                             }
2992                             scheduleNextAlarm(false /* do not remove alarms */);
2993                             triggerAppWidgetUpdate(id);
2994                         }
2995                     }
2996                 } else {
2997                     result = deleteEventInternal(id, callerIsSyncAdapter, true /* isBatch */);
2998                     scheduleNextAlarm(false /* do not remove alarms */);
2999                     triggerAppWidgetUpdate(id);
3000                 }
3001                 return result;
3002             }
3003             case ATTENDEES_ID: {
3004                 if (selection != null) {
3005                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
3006                 }
3007                 // Copy the attendee status value to the Events table.
3008                 updateEventAttendeeStatus(mDb, values);
3009 
3010                 if (callerIsSyncAdapter) {
3011                     long id = ContentUris.parseId(uri);
3012                     return mDb.update("Attendees", values, "_id=?",
3013                             new String[] {String.valueOf(id)});
3014                 } else {
3015                     return updateInTable("Attendees", values, uri, null /* selection */,
3016                             null /* selectionArgs */);
3017                 }
3018             }
3019             case CALENDAR_ALERTS_ID: {
3020                 if (selection != null) {
3021                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
3022                 }
3023                 // Note: dirty bit is not set for Alerts because it is not synced.
3024                 // It is generated from Reminders, which is synced.
3025                 long id = ContentUris.parseId(uri);
3026                 return mDb.update("CalendarAlerts", values, "_id=?",
3027                         new String[] {String.valueOf(id)});
3028             }
3029             case CALENDAR_ALERTS: {
3030                 // Note: dirty bit is not set for Alerts because it is not synced.
3031                 // It is generated from Reminders, which is synced.
3032                 return mDb.update("CalendarAlerts", values, selection, selectionArgs);
3033             }
3034             case REMINDERS_ID: {
3035                 if (selection != null) {
3036                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
3037                 }
3038                 if (callerIsSyncAdapter) {
3039                     long id = ContentUris.parseId(uri);
3040                     count = mDb.update("Reminders", values, "_id=?",
3041                             new String[] {String.valueOf(id)});
3042                 } else {
3043                     count = updateInTable("Reminders", values, uri, null /* selection */,
3044                             null /* selectionArgs */);
3045                 }
3046 
3047                 // Reschedule the event alarms because the
3048                 // "minutes" field may have changed.
3049                 if (Log.isLoggable(TAG, Log.DEBUG)) {
3050                     Log.d(TAG, "updateInternal() changing reminder");
3051                 }
3052                 scheduleNextAlarm(false /* do not remove alarms */);
3053                 return count;
3054             }
3055             case EXTENDED_PROPERTIES_ID: {
3056                 if (selection != null) {
3057                     throw new UnsupportedOperationException("Selection not permitted for " + uri);
3058                 }
3059                 if (callerIsSyncAdapter) {
3060                     long id = ContentUris.parseId(uri);
3061                     return mDb.update("ExtendedProperties", values, "_id=?",
3062                             new String[] {String.valueOf(id)});
3063                 } else {
3064                     return updateInTable("ExtendedProperties", values, uri, null /* selection */,
3065                             null /* selectionArgs */);
3066                 }
3067             }
3068             // TODO: replace the SCHEDULE_ALARM private URIs with a
3069             // service
3070             case SCHEDULE_ALARM: {
3071                 scheduleNextAlarm(false);
3072                 return 0;
3073             }
3074             case SCHEDULE_ALARM_REMOVE: {
3075                 scheduleNextAlarm(true);
3076                 return 0;
3077             }
3078 
3079             case PROVIDER_PROPERTIES: {
3080                 if (selection == null) {
3081                     throw new UnsupportedOperationException("Selection cannot be null for " + uri);
3082                 }
3083                 if (!selection.equals("key=?")) {
3084                     throw new UnsupportedOperationException("Selection should be key=? for " + uri);
3085                 }
3086 
3087                 List<String> list = Arrays.asList(selectionArgs);
3088 
3089                 if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)) {
3090                     throw new UnsupportedOperationException("Invalid selection key: " +
3091                             CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS + " for " + uri);
3092                 }
3093 
3094                 // Before it may be changed, save current Instances timezone for later use
3095                 String timezoneInstancesBeforeUpdate = mCalendarCache.readTimezoneInstances();
3096 
3097                 // Update the database with the provided values (this call may change the value
3098                 // of timezone Instances)
3099                 int result = mDb.update("CalendarCache", values, selection, selectionArgs);
3100 
3101                 // if successful, do some house cleaning:
3102                 // if the timezone type is set to "home", set the Instances timezone to the previous
3103                 // if the timezone type is set to "auto", set the Instances timezone to the current
3104                 //      device one
3105                 // if the timezone Instances is set AND if we are in "home" timezone type, then
3106                 //      save the timezone Instance into "previous" too
3107                 if (result > 0) {
3108                     // If we are changing timezone type...
3109                     if (list.contains(CalendarCache.KEY_TIMEZONE_TYPE)) {
3110                         String value = values.getAsString(CalendarCache.COLUMN_NAME_VALUE);
3111                         if (value != null) {
3112                             // if we are setting timezone type to "home"
3113                             if (value.equals(CalendarCache.TIMEZONE_TYPE_HOME)) {
3114                                 String previousTimezone =
3115                                         mCalendarCache.readTimezoneInstancesPrevious();
3116                                 if (previousTimezone != null) {
3117                                     mCalendarCache.writeTimezoneInstances(previousTimezone);
3118                                 }
3119                                 // Regenerate Instances if the "home" timezone has changed
3120                                 if (!timezoneInstancesBeforeUpdate.equals(previousTimezone) ) {
3121                                     regenerateInstancesTable();
3122                                 }
3123                             }
3124                             // if we are setting timezone type to "auto"
3125                             else if (value.equals(CalendarCache.TIMEZONE_TYPE_AUTO)) {
3126                                 String localTimezone = TimeZone.getDefault().getID();
3127                                 mCalendarCache.writeTimezoneInstances(localTimezone);
3128                                 if (!timezoneInstancesBeforeUpdate.equals(localTimezone)) {
3129                                     regenerateInstancesTable();
3130                                 }
3131                             }
3132                         }
3133                     }
3134                     // If we are changing timezone Instances...
3135                     else if (list.contains(CalendarCache.KEY_TIMEZONE_INSTANCES)) {
3136                         // if we are in "home" timezone type...
3137                         if (isHomeTimezone()) {
3138                             String timezoneInstances = mCalendarCache.readTimezoneInstances();
3139                             // Update the previous value
3140                             mCalendarCache.writeTimezoneInstancesPrevious(timezoneInstances);
3141                             // Recompute Instances if the "home" timezone has changed
3142                             if (timezoneInstancesBeforeUpdate != null &&
3143                                     !timezoneInstancesBeforeUpdate.equals(timezoneInstances)) {
3144                                 regenerateInstancesTable();
3145                             }
3146                         }
3147                     }
3148                     triggerAppWidgetUpdate(-1);
3149                 }
3150                 return result;
3151             }
3152 
3153             default:
3154                 throw new IllegalArgumentException("Unknown URL " + uri);
3155         }
3156     }
3157 
appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri)3158     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
3159         final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
3160         final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
3161         if (!TextUtils.isEmpty(accountName)) {
3162             qb.appendWhere(Calendar.Calendars._SYNC_ACCOUNT + "="
3163                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3164                     + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
3165                     + DatabaseUtils.sqlEscapeString(accountType));
3166         } else {
3167             qb.appendWhere("1"); // I.e. always true
3168         }
3169     }
3170 
appendAccountToSelection(Uri uri, String selection)3171     private String appendAccountToSelection(Uri uri, String selection) {
3172         final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
3173         final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
3174         if (!TextUtils.isEmpty(accountName)) {
3175             StringBuilder selectionSb = new StringBuilder(Calendar.Calendars._SYNC_ACCOUNT + "="
3176                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
3177                     + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
3178                     + DatabaseUtils.sqlEscapeString(accountType));
3179             if (!TextUtils.isEmpty(selection)) {
3180                 selectionSb.append(" AND (");
3181                 selectionSb.append(selection);
3182                 selectionSb.append(')');
3183             }
3184             return selectionSb.toString();
3185         } else {
3186             return selection;
3187         }
3188     }
3189 
modifyCalendarSubscription(long id, boolean syncEvents)3190     private void modifyCalendarSubscription(long id, boolean syncEvents) {
3191         // get the account, url, and current selected state
3192         // for this calendar.
3193         Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
3194                 new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
3195                         Calendars.URL, Calendars.SYNC_EVENTS},
3196                 null /* selection */,
3197                 null /* selectionArgs */,
3198                 null /* sort */);
3199 
3200         Account account = null;
3201         String calendarUrl = null;
3202         boolean oldSyncEvents = false;
3203         if (cursor != null) {
3204             try {
3205                 if (cursor.moveToFirst()) {
3206                     final String accountName = cursor.getString(0);
3207                     final String accountType = cursor.getString(1);
3208                     account = new Account(accountName, accountType);
3209                     calendarUrl = cursor.getString(2);
3210                     oldSyncEvents = (cursor.getInt(3) != 0);
3211                 }
3212             } finally {
3213                 cursor.close();
3214             }
3215         }
3216 
3217         if (account == null) {
3218             // should not happen?
3219             if (Log.isLoggable(TAG, Log.WARN)) {
3220                 Log.w(TAG, "Cannot update subscription because account "
3221                         + "is empty -- should not happen.");
3222             }
3223             return;
3224         }
3225 
3226         if (TextUtils.isEmpty(calendarUrl)) {
3227             // Passing in a null Url will cause it to not add any extras
3228             // Should only happen for non-google calendars.
3229             calendarUrl = null;
3230         }
3231 
3232         if (oldSyncEvents == syncEvents) {
3233             // nothing to do
3234             return;
3235         }
3236 
3237         // If the calendar is not selected for syncing, then don't download
3238         // events.
3239         mDbHelper.scheduleSync(account, !syncEvents, calendarUrl);
3240     }
3241 
3242     // TODO: is this needed
3243 //    @Override
3244 //    public void onSyncStop(SyncContext context, boolean success) {
3245 //        super.onSyncStop(context, success);
3246 //        if (Log.isLoggable(TAG, Log.DEBUG)) {
3247 //            Log.d(TAG, "onSyncStop() success: " + success);
3248 //        }
3249 //        scheduleNextAlarm(false /* do not remove alarms */);
3250 //        triggerAppWidgetUpdate(-1);
3251 //    }
3252 
3253     /**
3254      * Update any existing widgets with the changed events.
3255      *
3256      * @param changedEventId Specific event known to be changed, otherwise -1.
3257      *            If present, we use it to decide if an update is necessary.
3258      */
triggerAppWidgetUpdate(long changedEventId)3259     private synchronized void triggerAppWidgetUpdate(long changedEventId) {
3260         Context context = getContext();
3261         if (context != null) {
3262             mAppWidgetProvider.providerUpdated(context, changedEventId);
3263         }
3264     }
3265 
3266     /* Retrieve and cache the alarm manager */
getAlarmManager()3267     private AlarmManager getAlarmManager() {
3268         synchronized(mAlarmLock) {
3269             if (mAlarmManager == null) {
3270                 Context context = getContext();
3271                 if (context == null) {
3272                     if (Log.isLoggable(TAG, Log.ERROR)) {
3273                         Log.e(TAG, "getAlarmManager() cannot get Context");
3274                     }
3275                     return null;
3276                 }
3277                 Object service = context.getSystemService(Context.ALARM_SERVICE);
3278                 mAlarmManager = (AlarmManager) service;
3279             }
3280             return mAlarmManager;
3281         }
3282     }
3283 
scheduleNextAlarmCheck(long triggerTime)3284     void scheduleNextAlarmCheck(long triggerTime) {
3285         AlarmManager manager = getAlarmManager();
3286         if (manager == null) {
3287             if (Log.isLoggable(TAG, Log.ERROR)) {
3288                 Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
3289             }
3290             return;
3291         }
3292         Context context = getContext();
3293         Intent intent = new Intent(CalendarReceiver.SCHEDULE);
3294         intent.setClass(context, CalendarReceiver.class);
3295         PendingIntent pending = PendingIntent.getBroadcast(context,
3296                 0, intent, PendingIntent.FLAG_NO_CREATE);
3297         if (pending != null) {
3298             // Cancel any previous alarms that do the same thing.
3299             manager.cancel(pending);
3300         }
3301         pending = PendingIntent.getBroadcast(context,
3302                 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
3303 
3304         if (Log.isLoggable(TAG, Log.DEBUG)) {
3305             Time time = new Time();
3306             time.set(triggerTime);
3307             String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3308             Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
3309         }
3310 
3311         manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
3312     }
3313 
3314     /*
3315      * This method runs the alarm scheduler in a background thread.
3316      */
scheduleNextAlarm(boolean removeAlarms)3317     void scheduleNextAlarm(boolean removeAlarms) {
3318         synchronized (mAlarmLock) {
3319             if (mAlarmScheduler == null) {
3320                 mAlarmScheduler = new AlarmScheduler(removeAlarms);
3321                 mAlarmScheduler.start();
3322             } else {
3323                 mRerunAlarmScheduler = true;
3324                 // removing the alarms is a stronger action so it has
3325                 // precedence.
3326                 mRemoveAlarmsOnRerun = mRemoveAlarmsOnRerun || removeAlarms;
3327             }
3328         }
3329     }
3330 
3331     /**
3332      * This method runs in a background thread and schedules an alarm for
3333      * the next calendar event, if necessary.
3334      */
runScheduleNextAlarm(boolean removeAlarms)3335     private void runScheduleNextAlarm(boolean removeAlarms) {
3336         final SQLiteDatabase db = mDbHelper.getWritableDatabase();
3337         db.beginTransaction();
3338         try {
3339             if (removeAlarms) {
3340                 removeScheduledAlarmsLocked(db);
3341             }
3342             scheduleNextAlarmLocked(db);
3343             db.setTransactionSuccessful();
3344         } finally {
3345             db.endTransaction();
3346         }
3347     }
3348 
3349     /**
3350      * This method looks at the 24-hour window from now for any events that it
3351      * needs to schedule.  This method runs within a database transaction. It
3352      * also runs in a background thread.
3353      *
3354      * The CalendarProvider2 keeps track of which alarms it has already scheduled
3355      * to avoid scheduling them more than once and for debugging problems with
3356      * alarms.  It stores this knowledge in a database table called CalendarAlerts
3357      * which persists across reboots.  But the actual alarm list is in memory
3358      * and disappears if the phone loses power.  To avoid missing an alarm, we
3359      * clear the entries in the CalendarAlerts table when we start up the
3360      * CalendarProvider2.
3361      *
3362      * Scheduling an alarm multiple times is not tragic -- we filter out the
3363      * extra ones when we receive them. But we still need to keep track of the
3364      * scheduled alarms. The main reason is that we need to prevent multiple
3365      * notifications for the same alarm (on the receive side) in case we
3366      * accidentally schedule the same alarm multiple times.  We don't have
3367      * visibility into the system's alarm list so we can never know for sure if
3368      * we have already scheduled an alarm and it's better to err on scheduling
3369      * an alarm twice rather than missing an alarm.  Another reason we keep
3370      * track of scheduled alarms in a database table is that it makes it easy to
3371      * run an SQL query to find the next reminder that we haven't scheduled.
3372      *
3373      * @param db the database
3374      */
scheduleNextAlarmLocked(SQLiteDatabase db)3375     private void scheduleNextAlarmLocked(SQLiteDatabase db) {
3376         AlarmManager alarmManager = getAlarmManager();
3377         if (alarmManager == null) {
3378             if (Log.isLoggable(TAG, Log.ERROR)) {
3379                 Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
3380             }
3381             return;
3382         }
3383 
3384         final long currentMillis = System.currentTimeMillis();
3385         final long start = currentMillis - SCHEDULE_ALARM_SLACK;
3386         final long end = start + (24 * 60 * 60 * 1000);
3387         ContentResolver cr = getContext().getContentResolver();
3388         if (Log.isLoggable(TAG, Log.DEBUG)) {
3389             Time time = new Time();
3390             time.set(start);
3391             String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3392             Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
3393         }
3394 
3395         // Delete rows in CalendarAlert where the corresponding Instance or
3396         // Reminder no longer exist.
3397         // Also clear old alarms but keep alarms around for a while to prevent
3398         // multiple alerts for the same reminder.  The "clearUpToTime'
3399         // should be further in the past than the point in time where
3400         // we start searching for events (the "start" variable defined above).
3401         String selectArg[] = new String[] {
3402             Long.toString(currentMillis - CLEAR_OLD_ALARM_THRESHOLD)
3403         };
3404 
3405         int rowsDeleted =
3406             db.delete(CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg);
3407 
3408         long nextAlarmTime = end;
3409         final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
3410         if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) {
3411             nextAlarmTime = tmpAlarmTime;
3412         }
3413 
3414         // Extract events from the database sorted by alarm time.  The
3415         // alarm times are computed from Instances.begin (whose units
3416         // are milliseconds) and Reminders.minutes (whose units are
3417         // minutes).
3418         //
3419         // Also, ignore events whose end time is already in the past.
3420         // Also, ignore events alarms that we have already scheduled.
3421         //
3422         // Note 1: we can add support for the case where Reminders.minutes
3423         // equals -1 to mean use Calendars.minutes by adding a UNION for
3424         // that case where the two halves restrict the WHERE clause on
3425         // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
3426         //
3427         // Note 2: we have to name "myAlarmTime" different from the
3428         // "alarmTime" column in CalendarAlerts because otherwise the
3429         // query won't find multiple alarms for the same event.
3430         //
3431         // The CAST is needed in the query because otherwise the expression
3432         // will be untyped and sqlite3's manifest typing will not convert the
3433         // string query parameter to an int in myAlarmtime>=?, so the comparison
3434         // will fail.  This could be simplified if bug 2464440 is resolved.
3435         String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
3436                 + " Instances.event_id AS eventId, begin, end,"
3437                 + " title, allDay, method, minutes"
3438                 + " FROM Instances INNER JOIN Events"
3439                 + " ON (Events._id = Instances.event_id)"
3440                 + " INNER JOIN Reminders"
3441                 + " ON (Instances.event_id = Reminders.event_id)"
3442                 + " WHERE method=" + Reminders.METHOD_ALERT
3443                 + " AND myAlarmTime>=CAST(? AS INT)"
3444                 + " AND myAlarmTime<=CAST(? AS INT)"
3445                 + " AND end>=?"
3446                 + " AND 0=(SELECT count(*) from CalendarAlerts CA"
3447                 + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
3448                 + " AND CA.alarmTime=myAlarmTime)"
3449                 + " ORDER BY myAlarmTime,begin,title";
3450         String queryParams[] = new String[] {String.valueOf(start), String.valueOf(nextAlarmTime),
3451                 String.valueOf(currentMillis)};
3452 
3453         String instancesTimezone = mCalendarCache.readTimezoneInstances();
3454         boolean isHomeTimezone = mCalendarCache.readTimezoneType().equals(
3455                 CalendarCache.TIMEZONE_TYPE_HOME);
3456         acquireInstanceRangeLocked(start,
3457                 end,
3458                 false /* don't use minimum expansion windows */,
3459                 false /* do not force Instances deletion and expansion */,
3460                 instancesTimezone,
3461                 isHomeTimezone);
3462         Cursor cursor = null;
3463         try {
3464             cursor = db.rawQuery(query, queryParams);
3465 
3466             final int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
3467             final int endIndex = cursor.getColumnIndex(Instances.END);
3468             final int eventIdIndex = cursor.getColumnIndex("eventId");
3469             final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
3470             final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
3471 
3472             if (Log.isLoggable(TAG, Log.DEBUG)) {
3473                 Time time = new Time();
3474                 time.set(nextAlarmTime);
3475                 String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3476                 Log.d(TAG, "cursor results: " + cursor.getCount() + " nextAlarmTime: "
3477                         + alarmTimeStr);
3478             }
3479 
3480             while (cursor.moveToNext()) {
3481                 // Schedule all alarms whose alarm time is as early as any
3482                 // scheduled alarm.  For example, if the earliest alarm is at
3483                 // 1pm, then we will schedule all alarms that occur at 1pm
3484                 // but no alarms that occur later than 1pm.
3485                 // Actually, we allow alarms up to a minute later to also
3486                 // be scheduled so that we don't have to check immediately
3487                 // again after an event alarm goes off.
3488                 final long alarmTime = cursor.getLong(alarmTimeIndex);
3489                 final long eventId = cursor.getLong(eventIdIndex);
3490                 final int minutes = cursor.getInt(minutesIndex);
3491                 final long startTime = cursor.getLong(beginIndex);
3492                 final long endTime = cursor.getLong(endIndex);
3493 
3494                 if (Log.isLoggable(TAG, Log.DEBUG)) {
3495                     Time time = new Time();
3496                     time.set(alarmTime);
3497                     String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
3498                     time.set(startTime);
3499                     String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
3500 
3501                     Log.d(TAG, "  looking at id: " + eventId + " " + startTime + startTimeStr
3502                             + " alarm: " + alarmTime + schedTime);
3503                 }
3504 
3505                 if (alarmTime < nextAlarmTime) {
3506                     nextAlarmTime = alarmTime;
3507                 } else if (alarmTime >
3508                            nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) {
3509                     // This event alarm (and all later ones) will be scheduled
3510                     // later.
3511                     if (Log.isLoggable(TAG, Log.DEBUG)) {
3512                         Log.d(TAG, "This event alarm (and all later ones) will be scheduled later");
3513                     }
3514                     break;
3515                 }
3516 
3517                 // Avoid an SQLiteContraintException by checking if this alarm
3518                 // already exists in the table.
3519                 if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
3520                     if (Log.isLoggable(TAG, Log.DEBUG)) {
3521                         int titleIndex = cursor.getColumnIndex(Events.TITLE);
3522                         String title = cursor.getString(titleIndex);
3523                         Log.d(TAG, "  alarm exists for id: " + eventId + " " + title);
3524                     }
3525                     continue;
3526                 }
3527 
3528                 // Insert this alarm into the CalendarAlerts table
3529                 Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
3530                         endTime, alarmTime, minutes);
3531                 if (uri == null) {
3532                     if (Log.isLoggable(TAG, Log.ERROR)) {
3533                         Log.e(TAG, "runScheduleNextAlarm() insert into "
3534                                 + "CalendarAlerts table failed");
3535                     }
3536                     continue;
3537                 }
3538 
3539                 CalendarAlerts.scheduleAlarm(getContext(), alarmManager, alarmTime);
3540             }
3541         } finally {
3542             if (cursor != null) {
3543                 cursor.close();
3544             }
3545         }
3546 
3547         // Refresh notification bar
3548         if (rowsDeleted > 0) {
3549             CalendarAlerts.scheduleAlarm(getContext(), alarmManager, currentMillis);
3550         }
3551 
3552         // If we scheduled an event alarm, then schedule the next alarm check
3553         // for one minute past that alarm.  Otherwise, if there were no
3554         // event alarms scheduled, then check again in 24 hours.  If a new
3555         // event is inserted before the next alarm check, then this method
3556         // will be run again when the new event is inserted.
3557         if (nextAlarmTime != Long.MAX_VALUE) {
3558             scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS);
3559         } else {
3560             scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS);
3561         }
3562     }
3563 
3564     /**
3565      * Removes the entries in the CalendarAlerts table for alarms that we have
3566      * scheduled but that have not fired yet. We do this to ensure that we
3567      * don't miss an alarm.  The CalendarAlerts table keeps track of the
3568      * alarms that we have scheduled but the actual alarm list is in memory
3569      * and will be cleared if the phone reboots.
3570      *
3571      * We don't need to remove entries that have already fired, and in fact
3572      * we should not remove them because we need to display the notifications
3573      * until the user dismisses them.
3574      *
3575      * We could remove entries that have fired and been dismissed, but we leave
3576      * them around for a while because it makes it easier to debug problems.
3577      * Entries that are old enough will be cleaned up later when we schedule
3578      * new alarms.
3579      */
removeScheduledAlarmsLocked(SQLiteDatabase db)3580     private void removeScheduledAlarmsLocked(SQLiteDatabase db) {
3581         if (Log.isLoggable(TAG, Log.DEBUG)) {
3582             Log.d(TAG, "removing scheduled alarms");
3583         }
3584         db.delete(CalendarAlerts.TABLE_NAME,
3585                 CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
3586     }
3587 
3588     private static String sEventsTable = "Events";
3589     private static String sAttendeesTable = "Attendees";
3590     private static String sRemindersTable = "Reminders";
3591     private static String sCalendarAlertsTable = "CalendarAlerts";
3592     private static String sExtendedPropertiesTable = "ExtendedProperties";
3593 
3594     private static final int EVENTS = 1;
3595     private static final int EVENTS_ID = 2;
3596     private static final int INSTANCES = 3;
3597     private static final int DELETED_EVENTS = 4;
3598     private static final int CALENDARS = 5;
3599     private static final int CALENDARS_ID = 6;
3600     private static final int ATTENDEES = 7;
3601     private static final int ATTENDEES_ID = 8;
3602     private static final int REMINDERS = 9;
3603     private static final int REMINDERS_ID = 10;
3604     private static final int EXTENDED_PROPERTIES = 11;
3605     private static final int EXTENDED_PROPERTIES_ID = 12;
3606     private static final int CALENDAR_ALERTS = 13;
3607     private static final int CALENDAR_ALERTS_ID = 14;
3608     private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
3609     private static final int INSTANCES_BY_DAY = 16;
3610     private static final int SYNCSTATE = 17;
3611     private static final int SYNCSTATE_ID = 18;
3612     private static final int EVENT_ENTITIES = 19;
3613     private static final int EVENT_ENTITIES_ID = 20;
3614     private static final int EVENT_DAYS = 21;
3615     private static final int SCHEDULE_ALARM = 22;
3616     private static final int SCHEDULE_ALARM_REMOVE = 23;
3617     private static final int TIME = 24;
3618     private static final int PROVIDER_PROPERTIES = 25;
3619 
3620     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
3621     private static final HashMap<String, String> sInstancesProjectionMap;
3622     private static final HashMap<String, String> sEventsProjectionMap;
3623     private static final HashMap<String, String> sEventEntitiesProjectionMap;
3624     private static final HashMap<String, String> sAttendeesProjectionMap;
3625     private static final HashMap<String, String> sRemindersProjectionMap;
3626     private static final HashMap<String, String> sCalendarAlertsProjectionMap;
3627     private static final HashMap<String, String> sCalendarCacheProjectionMap;
3628 
3629     static {
sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES)3630         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES);
sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY)3631         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY);
sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS)3632         sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS);
sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS)3633         sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS);
sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID)3634         sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES)3635         sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES);
sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID)3636         sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS)3637         sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS);
sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID)3638         sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS)3639         sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS);
sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES)3640         sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES);
sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID)3641         sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS)3642         sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS);
sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID)3643         sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES)3644         sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES);
sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID)3645         sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS)3646         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS);
sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID)3647         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE)3648         sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance",
3649                            CALENDAR_ALERTS_BY_INSTANCE);
sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE)3650         sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE);
sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID)3651         sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM)3652         sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM);
sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE)3653         sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE);
sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME)3654         sUriMatcher.addURI(Calendar.AUTHORITY, "time/#", TIME);
sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME)3655         sUriMatcher.addURI(Calendar.AUTHORITY, "time", TIME);
sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES)3656         sUriMatcher.addURI(Calendar.AUTHORITY, "properties", PROVIDER_PROPERTIES);
3657 
3658         sEventsProjectionMap = new HashMap<String, String>();
3659         // Events columns
sEventsProjectionMap.put(Events.HTML_URI, "htmlUri")3660         sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
sEventsProjectionMap.put(Events.TITLE, "title")3661         sEventsProjectionMap.put(Events.TITLE, "title");
sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3662         sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
sEventsProjectionMap.put(Events.DESCRIPTION, "description")3663         sEventsProjectionMap.put(Events.DESCRIPTION, "description");
sEventsProjectionMap.put(Events.STATUS, "eventStatus")3664         sEventsProjectionMap.put(Events.STATUS, "eventStatus");
sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3665         sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3666         sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
sEventsProjectionMap.put(Events.DTSTART, "dtstart")3667         sEventsProjectionMap.put(Events.DTSTART, "dtstart");
sEventsProjectionMap.put(Events.DTEND, "dtend")3668         sEventsProjectionMap.put(Events.DTEND, "dtend");
sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3669         sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
sEventsProjectionMap.put(Events.DURATION, "duration")3670         sEventsProjectionMap.put(Events.DURATION, "duration");
sEventsProjectionMap.put(Events.ALL_DAY, "allDay")3671         sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
sEventsProjectionMap.put(Events.VISIBILITY, "visibility")3672         sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency")3673         sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm")3674         sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")3675         sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
sEventsProjectionMap.put(Events.RRULE, "rrule")3676         sEventsProjectionMap.put(Events.RRULE, "rrule");
sEventsProjectionMap.put(Events.RDATE, "rdate")3677         sEventsProjectionMap.put(Events.RDATE, "rdate");
sEventsProjectionMap.put(Events.EXRULE, "exrule")3678         sEventsProjectionMap.put(Events.EXRULE, "exrule");
sEventsProjectionMap.put(Events.EXDATE, "exdate")3679         sEventsProjectionMap.put(Events.EXDATE, "exdate");
sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")3680         sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")3681         sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")3682         sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
sEventsProjectionMap.put(Events.LAST_DATE, "lastDate")3683         sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")3684         sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id")3685         sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")3686         sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")3687         sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")3688         sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
sEventsProjectionMap.put(Events.ORGANIZER, "organizer")3689         sEventsProjectionMap.put(Events.ORGANIZER, "organizer");
sEventsProjectionMap.put(Events.DELETED, "deleted")3690         sEventsProjectionMap.put(Events.DELETED, "deleted");
3691 
3692         // Put the shared items into the Attendees, Reminders projection map
3693         sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3694         sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3695 
3696         // Calendar columns
sEventsProjectionMap.put(Calendars.COLOR, "color")3697         sEventsProjectionMap.put(Calendars.COLOR, "color");
sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level")3698         sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level");
sEventsProjectionMap.put(Calendars.SELECTED, "selected")3699         sEventsProjectionMap.put(Calendars.SELECTED, "selected");
sEventsProjectionMap.put(Calendars.URL, "url")3700         sEventsProjectionMap.put(Calendars.URL, "url");
sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone")3701         sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount")3702         sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount");
3703 
3704         // Put the shared items into the Instances projection map
3705         // The Instances and CalendarAlerts are joined with Calendars, so the projections include
3706         // the above Calendar columns.
3707         sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3708         sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
3709 
sEventsProjectionMap.put(Events._ID, "_id")3710         sEventsProjectionMap.put(Events._ID, "_id");
sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id")3711         sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id");
sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version")3712         sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version");
sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time")3713         sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time");
sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id")3714         sEventsProjectionMap.put(Events._SYNC_DATA, "_sync_local_id");
sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty")3715         sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty");
sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account")3716         sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account");
sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE, "_sync_account_type")3717         sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
3718                 "_sync_account_type");
3719 
3720         sEventEntitiesProjectionMap = new HashMap<String, String>();
sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri")3721         sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri");
sEventEntitiesProjectionMap.put(Events.TITLE, "title")3722         sEventEntitiesProjectionMap.put(Events.TITLE, "title");
sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description")3723         sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description");
sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation")3724         sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus")3725         sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus");
sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus")3726         sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri")3727         sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart")3728         sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart");
sEventEntitiesProjectionMap.put(Events.DTEND, "dtend")3729         sEventEntitiesProjectionMap.put(Events.DTEND, "dtend");
sEventEntitiesProjectionMap.put(Events.DURATION, "duration")3730         sEventEntitiesProjectionMap.put(Events.DURATION, "duration");
sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone")3731         sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay")3732         sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay");
sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility")3733         sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility");
sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency")3734         sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency");
sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm")3735         sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties")3736         sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
sEventEntitiesProjectionMap.put(Events.RRULE, "rrule")3737         sEventEntitiesProjectionMap.put(Events.RRULE, "rrule");
sEventEntitiesProjectionMap.put(Events.RDATE, "rdate")3738         sEventEntitiesProjectionMap.put(Events.RDATE, "rdate");
sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule")3739         sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule");
sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate")3740         sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate");
sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent")3741         sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime")3742         sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay")3743         sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate")3744         sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate");
sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData")3745         sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id")3746         sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers")3747         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify")3748         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests")3749         sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer")3750         sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer");
sEventEntitiesProjectionMap.put(Events.DELETED, "deleted")3751         sEventEntitiesProjectionMap.put(Events.DELETED, "deleted");
sEventEntitiesProjectionMap.put(Events._ID, Events._ID)3752         sEventEntitiesProjectionMap.put(Events._ID, Events._ID);
sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID)3753         sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID);
sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA)3754         sEventEntitiesProjectionMap.put(Events._SYNC_DATA, Events._SYNC_DATA);
sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION)3755         sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION);
sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY)3756         sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY);
sEventEntitiesProjectionMap.put(Calendars.URL, "url")3757         sEventEntitiesProjectionMap.put(Calendars.URL, "url");
3758 
3759         // Instances columns
sInstancesProjectionMap.put(Instances.BEGIN, "begin")3760         sInstancesProjectionMap.put(Instances.BEGIN, "begin");
sInstancesProjectionMap.put(Instances.END, "end")3761         sInstancesProjectionMap.put(Instances.END, "end");
sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id")3762         sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id")3763         sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
sInstancesProjectionMap.put(Instances.START_DAY, "startDay")3764         sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
sInstancesProjectionMap.put(Instances.END_DAY, "endDay")3765         sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute")3766         sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute")3767         sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
3768 
3769         // Attendees columns
sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id")3770         sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id")3771         sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName")3772         sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail")3773         sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus")3774         sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship")3775         sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType")3776         sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
3777 
3778         // Reminders columns
sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id")3779         sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id")3780         sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
sRemindersProjectionMap.put(Reminders.MINUTES, "minutes")3781         sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
sRemindersProjectionMap.put(Reminders.METHOD, "method")3782         sRemindersProjectionMap.put(Reminders.METHOD, "method");
3783 
3784         // CalendarAlerts columns
sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id")3785         sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id")3786         sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin")3787         sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end")3788         sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime")3789         sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state")3790         sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes")3791         sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
3792 
3793         // CalendarCache columns
3794         sCalendarCacheProjectionMap = new HashMap<String, String>();
sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key")3795         sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_KEY, "key");
sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value")3796         sCalendarCacheProjectionMap.put(CalendarCache.COLUMN_NAME_VALUE, "value");
3797     }
3798 
3799     /**
3800      * Make sure that there are no entries for accounts that no longer
3801      * exist. We are overriding this since we need to delete from the
3802      * Calendars table, which is not syncable, which has triggers that
3803      * will delete from the Events and  tables, which are
3804      * syncable.  TODO: update comment, make sure deletes don't get synced.
3805      */
onAccountsUpdated(Account[] accounts)3806     public void onAccountsUpdated(Account[] accounts) {
3807         mDb = mDbHelper.getWritableDatabase();
3808         if (mDb == null) return;
3809 
3810         HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>();
3811         HashSet<Account> validAccounts = new HashSet<Account>();
3812         for (Account account : accounts) {
3813             validAccounts.add(new Account(account.name, account.type));
3814             accountHasCalendar.put(account, false);
3815         }
3816         ArrayList<Account> accountsToDelete = new ArrayList<Account>();
3817 
3818         mDb.beginTransaction();
3819         try {
3820 
3821             for (String table : new String[]{"Calendars"}) {
3822                 // Find all the accounts the contacts DB knows about, mark the ones that aren't
3823                 // in the valid set for deletion.
3824                 Cursor c = mDb.rawQuery("SELECT DISTINCT " + CalendarDatabaseHelper.ACCOUNT_NAME
3825                                         + ","
3826                                         + CalendarDatabaseHelper.ACCOUNT_TYPE + " from "
3827                         + table, null);
3828                 while (c.moveToNext()) {
3829                     if (c.getString(0) != null && c.getString(1) != null) {
3830                         Account currAccount = new Account(c.getString(0), c.getString(1));
3831                         if (!validAccounts.contains(currAccount)) {
3832                             accountsToDelete.add(currAccount);
3833                         }
3834                     }
3835                 }
3836                 c.close();
3837             }
3838 
3839             for (Account account : accountsToDelete) {
3840                 if (Log.isLoggable(TAG, Log.DEBUG)) {
3841                     Log.d(TAG, "removing data for removed account " + account);
3842                 }
3843                 String[] params = new String[]{account.name, account.type};
3844                 mDb.execSQL("DELETE FROM Calendars"
3845                         + " WHERE " + CalendarDatabaseHelper.ACCOUNT_NAME + "= ? AND "
3846                         + CalendarDatabaseHelper.ACCOUNT_TYPE
3847                         + "= ?", params);
3848             }
3849             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
3850             mDb.setTransactionSuccessful();
3851         } finally {
3852             mDb.endTransaction();
3853         }
3854     }
3855 
readBooleanQueryParameter(Uri uri, String name, boolean defaultValue)3856     /* package */ static boolean readBooleanQueryParameter(Uri uri, String name,
3857             boolean defaultValue) {
3858         final String flag = getQueryParameter(uri, name);
3859         return flag == null
3860                 ? defaultValue
3861                 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
3862     }
3863 
3864     // Duplicated from ContactsProvider2.  TODO: a utility class for shared code
3865     /**
3866      * A fast re-implementation of {@link Uri#getQueryParameter}
3867      */
getQueryParameter(Uri uri, String parameter)3868     /* package */ static String getQueryParameter(Uri uri, String parameter) {
3869         String query = uri.getEncodedQuery();
3870         if (query == null) {
3871             return null;
3872         }
3873 
3874         int queryLength = query.length();
3875         int parameterLength = parameter.length();
3876 
3877         String value;
3878         int index = 0;
3879         while (true) {
3880             index = query.indexOf(parameter, index);
3881             if (index == -1) {
3882                 return null;
3883             }
3884 
3885             index += parameterLength;
3886 
3887             if (queryLength == index) {
3888                 return null;
3889             }
3890 
3891             if (query.charAt(index) == '=') {
3892                 index++;
3893                 break;
3894             }
3895         }
3896 
3897         int ampIndex = query.indexOf('&', index);
3898         if (ampIndex == -1) {
3899             value = query.substring(index);
3900         } else {
3901             value = query.substring(index, ampIndex);
3902         }
3903 
3904         return Uri.decode(value);
3905     }
3906 
3907     /**
3908      * Inserts an argument at the beginning of the selection arg list.
3909      *
3910      * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is
3911      * prepended to the user's where clause (combined with 'AND') to generate
3912      * the final where close, so arguments associated with the QueryBuilder are
3913      * prepended before any user selection args to keep them in the right order.
3914      */
insertSelectionArg(String[] selectionArgs, String arg)3915     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
3916         if (selectionArgs == null) {
3917             return new String[] {arg};
3918         } else {
3919             int newLength = selectionArgs.length + 1;
3920             String[] newSelectionArgs = new String[newLength];
3921             newSelectionArgs[0] = arg;
3922             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
3923             return newSelectionArgs;
3924         }
3925     }
3926 }
3927