1f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik/*
2f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * Copyright (C) 2011 The Android Open Source Project
3f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik *
4f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * Licensed under the Apache License, Version 2.0 (the "License");
5f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * you may not use this file except in compliance with the License.
6f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * You may obtain a copy of the License at
7f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik *
8f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik *      http://www.apache.org/licenses/LICENSE-2.0
9f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik *
10f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * Unless required by applicable law or agreed to in writing, software
11f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * distributed under the License is distributed on an "AS IS" BASIS,
12f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * See the License for the specific language governing permissions and
14f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik * limitations under the License.
15f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik */
16f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
17f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikpackage com.android.providers.calendar;
18f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
195cd969369a0e025bad07ad32bda9c8c4f0630457Michael Chanimport com.android.calendarcommon2.DateException;
205cd969369a0e025bad07ad32bda9c8c4f0630457Michael Chanimport com.android.calendarcommon2.Duration;
215cd969369a0e025bad07ad32bda9c8c4f0630457Michael Chanimport com.android.calendarcommon2.EventRecurrence;
225cd969369a0e025bad07ad32bda9c8c4f0630457Michael Chanimport com.android.calendarcommon2.RecurrenceProcessor;
235cd969369a0e025bad07ad32bda9c8c4f0630457Michael Chanimport com.android.calendarcommon2.RecurrenceSet;
24f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport com.android.providers.calendar.CalendarDatabaseHelper.Tables;
2593e0bbb921cce7a5cec355521bc570c03c9d6a1cAndy McFadden
26f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.content.ContentValues;
27f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.database.Cursor;
28f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.database.DatabaseUtils;
29f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.database.sqlite.SQLiteDatabase;
30f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.database.sqlite.SQLiteQueryBuilder;
31f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.os.Debug;
32b9644fe24edf9e25f0b21c1394e88d25070e0238RoboErikimport android.provider.CalendarContract.Calendars;
33b9644fe24edf9e25f0b21c1394e88d25070e0238RoboErikimport android.provider.CalendarContract.Events;
34b9644fe24edf9e25f0b21c1394e88d25070e0238RoboErikimport android.provider.CalendarContract.Instances;
35f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.text.TextUtils;
36f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.text.format.Time;
37f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.util.Log;
38f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport android.util.TimeFormatException;
39f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
40f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport java.util.ArrayList;
41f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport java.util.HashMap;
42f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikimport java.util.Set;
43f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
44f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErikpublic class CalendarInstancesHelper {
45f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    public static final class EventInstancesMap extends
46f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            HashMap<String, CalendarInstancesHelper.InstancesList> {
47f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        public void add(String syncIdKey, ContentValues values) {
48f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            CalendarInstancesHelper.InstancesList instances = get(syncIdKey);
49f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (instances == null) {
50f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                instances = new CalendarInstancesHelper.InstancesList();
51f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                put(syncIdKey, instances);
52f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
53f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            instances.add(values);
54f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
55f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
56f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
57f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    public static final class InstancesList extends ArrayList<ContentValues> {
58f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
59f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
60f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final String TAG = "CalInstances";
61f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private CalendarDatabaseHelper mDbHelper;
62f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private SQLiteDatabase mDb;
63f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private MetaData mMetaData;
64f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private CalendarCache mCalendarCache;
65f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
66f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final String SQL_WHERE_GET_EVENTS_ENTRIES =
67f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            "((" + Events.DTSTART + " <= ? AND "
68f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    + "(" + Events.LAST_DATE + " IS NULL OR " + Events.LAST_DATE + " >= ?)) OR "
69f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            + "(" + Events.ORIGINAL_INSTANCE_TIME + " IS NOT NULL AND "
70f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    + Events.ORIGINAL_INSTANCE_TIME
71f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    + " <= ? AND " + Events.ORIGINAL_INSTANCE_TIME + " >= ?)) AND "
729ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert            + "(" + Calendars.SYNC_EVENTS + " != ?) AND "
739ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert            + "(" + Events.LAST_SYNCED + " = ?)";
74f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
75b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    /**
76b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Determines the set of Events where the _id matches the first query argument, or the
77b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * originalId matches the second argument.  Returns the _id field from the set of
78b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Instances whose event_id field matches one of those events.
79b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     */
80f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final String SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED =
812ce7955da7dffec7819ed38be85e72df8a6f33dcRoboErik            Instances._ID + " IN " +
822ce7955da7dffec7819ed38be85e72df8a6f33dcRoboErik            "(SELECT " + Tables.INSTANCES + "." + Instances._ID + " as _id" +
83f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " FROM " + Tables.INSTANCES +
84f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " INNER JOIN " + Tables.EVENTS +
85f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " ON (" +
86f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID +
87f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            ")" +
88b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            " WHERE " + Tables.EVENTS + "." + Events._ID + "=? OR " +
89b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                    Tables.EVENTS + "." + Events.ORIGINAL_ID + "=?)";
90f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
91b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    /**
92b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Determines the set of Events where the _sync_id matches the first query argument, or the
93b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * originalSyncId matches the second argument.  Returns the _id field from the set of
94b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Instances whose event_id field matches one of those events.
95b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     */
96f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final String SQL_WHERE_ID_FROM_INSTANCES_SYNCED =
972ce7955da7dffec7819ed38be85e72df8a6f33dcRoboErik            Instances._ID + " IN " +
982ce7955da7dffec7819ed38be85e72df8a6f33dcRoboErik            "(SELECT " + Tables.INSTANCES + "." + Instances._ID + " as _id" +
99f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " FROM " + Tables.INSTANCES +
100f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " INNER JOIN " + Tables.EVENTS +
101f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " ON (" +
102f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Tables.EVENTS + "." + Events._ID + "=" + Tables.INSTANCES + "." + Instances.EVENT_ID +
103f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            ")" +
104f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            " WHERE " + Tables.EVENTS + "." + Events._SYNC_ID + "=?" + " OR " +
105c8383567db3ade2aea28447ad3bd09ac3033bcd7RoboErik                    Tables.EVENTS + "." + Events.ORIGINAL_SYNC_ID + "=?)";
106f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
107f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final String[] EXPAND_COLUMNS = new String[] {
108f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events._ID,
109f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events._SYNC_ID,
110f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.STATUS,
111f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.DTSTART,
112f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.DTEND,
113f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.EVENT_TIMEZONE,
114f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.RRULE,
115f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.RDATE,
116f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.EXRULE,
117f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.EXDATE,
118f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.DURATION,
119f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.ALL_DAY,
120c8383567db3ade2aea28447ad3bd09ac3033bcd7RoboErik            Events.ORIGINAL_SYNC_ID,
121f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.ORIGINAL_INSTANCE_TIME,
122f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.CALENDAR_ID,
123f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Events.DELETED
124f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    };
125f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
126f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    // To determine if a recurrence exception originally overlapped the
127f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    // window, we need to assume a maximum duration, since we only know
128f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    // the original start time.
129f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private static final int MAX_ASSUMED_DURATION = 7 * 24 * 60 * 60 * 1000;
130f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
1314caf8d015918f619a67d321a152f150a01022717Andy McFadden    public CalendarInstancesHelper(CalendarDatabaseHelper calendarDbHelper, MetaData metaData) {
132f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        mDbHelper = calendarDbHelper;
133f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        mDb = mDbHelper.getWritableDatabase();
1344caf8d015918f619a67d321a152f150a01022717Andy McFadden        mMetaData = metaData;
135f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        mCalendarCache = new CalendarCache(mDbHelper);
136f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
137f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
138f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
139b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Extract the value from the specifed row and column of the Events table.
140b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     *
141b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * @param db The database to access.
142b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * @param rowId The Event's _id.
143b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * @param columnName The name of the column to access.
144b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * @return The value in string form.
145b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     */
146b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    private static String getEventValue(SQLiteDatabase db, long rowId, String columnName) {
147b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        String where = "SELECT " + columnName + " FROM " + Tables.EVENTS +
148b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            " WHERE " + Events._ID + "=?";
149b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        return DatabaseUtils.stringForQuery(db, where,
150b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                new String[] { String.valueOf(rowId) });
151b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    }
152b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
153b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    /**
154f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Perform instance expansion on the given entries.
155f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
156f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param begin Window start (ms).
157f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param end Window end (ms).
158f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param localTimezone
159f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param entries The entries to process.
160f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
161f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    protected void performInstanceExpansion(long begin, long end, String localTimezone,
162f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Cursor entries) {
163b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        // TODO: this only knows how to work with events that have been synced with the server
164f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        RecurrenceProcessor rp = new RecurrenceProcessor();
165f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
166f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Key into the instance values to hold the original event concatenated
167f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // with calendar id.
168f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        final String ORIGINAL_EVENT_AND_CALENDAR = "ORIGINAL_EVENT_AND_CALENDAR";
169f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
170f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int statusColumn = entries.getColumnIndex(Events.STATUS);
171f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
172f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int dtendColumn = entries.getColumnIndex(Events.DTEND);
173f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
174f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int durationColumn = entries.getColumnIndex(Events.DURATION);
175f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int rruleColumn = entries.getColumnIndex(Events.RRULE);
176f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int rdateColumn = entries.getColumnIndex(Events.RDATE);
177f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int exruleColumn = entries.getColumnIndex(Events.EXRULE);
178f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int exdateColumn = entries.getColumnIndex(Events.EXDATE);
179f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
180f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int idColumn = entries.getColumnIndex(Events._ID);
181f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
182c8383567db3ade2aea28447ad3bd09ac3033bcd7RoboErik        int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_SYNC_ID);
183f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
184f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int calendarIdColumn = entries.getColumnIndex(Events.CALENDAR_ID);
185f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int deletedColumn = entries.getColumnIndex(Events.DELETED);
186f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
187f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        ContentValues initialValues;
188f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        CalendarInstancesHelper.EventInstancesMap instancesMap =
189f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            new CalendarInstancesHelper.EventInstancesMap();
190f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
191f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Duration duration = new Duration();
192f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Time eventTime = new Time();
193f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
194f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Invariant: entries contains all events that affect the current
195f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // window.  It consists of:
196f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // a) Individual events that fall in the window.  These will be
197f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //    displayed.
198f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // b) Recurrences that included the window.  These will be displayed
199f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //    if not canceled.
200f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // c) Recurrence exceptions that fall in the window.  These will be
201f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //    displayed if not cancellations.
202f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // d) Recurrence exceptions that modify an instance inside the
203f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //    window (subject to 1 week assumption above), but are outside
204f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //    the window.  These will not be displayed.  Cases c and d are
205b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        //    distinguished by the start / end time.
206f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
207f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        while (entries.moveToNext()) {
208f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            try {
209f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                initialValues = null;
210f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
211f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                boolean allDay = entries.getInt(allDayColumn) != 0;
212f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
213f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String eventTimezone = entries.getString(eventTimezoneColumn);
214f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (allDay || TextUtils.isEmpty(eventTimezone)) {
215f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // in the events table, allDay events start at midnight.
216f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // this forces them to stay at midnight for all day events
217f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // TODO: check that this actually does the right thing.
218f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    eventTimezone = Time.TIMEZONE_UTC;
219f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
220f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
221f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                long dtstartMillis = entries.getLong(dtstartColumn);
222f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                Long eventId = Long.valueOf(entries.getLong(idColumn));
223f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
224f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String durationStr = entries.getString(durationColumn);
225f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (durationStr != null) {
226f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    try {
227f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.parse(durationStr);
228f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
229f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    catch (DateException e) {
230f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
231f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            Log.w(CalendarProvider2.TAG, "error parsing duration for event "
232f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                    + eventId + "'" + durationStr + "'", e);
233f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
234f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.sign = 1;
235f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.weeks = 0;
236f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.days = 0;
237f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.hours = 0;
238f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.minutes = 0;
239f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        duration.seconds = 0;
240f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        durationStr = "+P0S";
241f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
242f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
243f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
244f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String syncId = entries.getString(syncIdColumn);
245f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String originalEvent = entries.getString(originalEventColumn);
246f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
247f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                long originalInstanceTimeMillis = -1;
248f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (!entries.isNull(originalInstanceTimeColumn)) {
249f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
250f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
251f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                int status = entries.getInt(statusColumn);
252f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                boolean deleted = (entries.getInt(deletedColumn) != 0);
253f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
254f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String rruleStr = entries.getString(rruleColumn);
255f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String rdateStr = entries.getString(rdateColumn);
256f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String exruleStr = entries.getString(exruleColumn);
257f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String exdateStr = entries.getString(exdateColumn);
258f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                long calendarId = entries.getLong(calendarIdColumn);
259f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // key into instancesMap
260f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String syncIdKey = CalendarInstancesHelper.getSyncIdKey(syncId, calendarId);
261f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
262f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                RecurrenceSet recur = null;
263f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                try {
264f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
265f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                } catch (EventRecurrence.InvalidFormatException e) {
266f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
267f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        Log.w(CalendarProvider2.TAG, "Could not parse RRULE recurrence string: "
268f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                + rruleStr, e);
269f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
270f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    continue;
271f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
272f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
273f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (null != recur && recur.hasRecurrence()) {
274f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // the event is repeating
275f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
276f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (status == Events.STATUS_CANCELED) {
277f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // should not happen!
278f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
279f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            Log.e(CalendarProvider2.TAG, "Found canceled recurring event in "
280f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                    + "Events table.  Ignoring.");
281f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
282f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        continue;
283f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
284f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (deleted) {
285f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (Log.isLoggable(CalendarProvider2.TAG, Log.DEBUG)) {
286f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            Log.d(CalendarProvider2.TAG, "Found deleted recurring event in "
287f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                    + "Events table.  Ignoring.");
288f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
289f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        continue;
290f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
291f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
292f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // need to parse the event into a local calendar.
293f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    eventTime.timezone = eventTimezone;
294f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    eventTime.set(dtstartMillis);
295f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    eventTime.allDay = allDay;
296f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
297f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (durationStr == null) {
298f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // should not happen.
299f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
300f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            Log.e(CalendarProvider2.TAG, "Repeating event has no duration -- "
301f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                    + "should not happen.");
302f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
303f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (allDay) {
304f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            // set to one day.
305f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.sign = 1;
306f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.weeks = 0;
307f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.days = 1;
308f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.hours = 0;
309f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.minutes = 0;
310f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.seconds = 0;
311f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            durationStr = "+P1D";
312f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        } else {
313f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            // compute the duration from dtend, if we can.
314f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            // otherwise, use 0s.
315f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.sign = 1;
316f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.weeks = 0;
317f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.days = 0;
318f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.hours = 0;
319f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            duration.minutes = 0;
320f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            if (!entries.isNull(dtendColumn)) {
321f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                long dtendMillis = entries.getLong(dtendColumn);
322f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
323f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                durationStr = "+P" + duration.seconds + "S";
324f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            } else {
325f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                duration.seconds = 0;
326f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                durationStr = "+P0S";
327f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            }
328f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
329f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
330f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
331f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    long[] dates;
332f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    dates = rp.expand(eventTime, recur, begin, end);
333f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
334f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // Initialize the "eventTime" timezone outside the loop.
335f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // This is used in computeTimezoneDependentFields().
336f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (allDay) {
337f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        eventTime.timezone = Time.TIMEZONE_UTC;
338f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    } else {
339f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        eventTime.timezone = localTimezone;
340f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
341f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
342f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    long durationMillis = duration.getMillis();
343f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    for (long date : dates) {
344f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues = new ContentValues();
345f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(Instances.EVENT_ID, eventId);
346f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
347f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(Instances.BEGIN, date);
348f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        long dtendMillis = date + durationMillis;
349f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(Instances.END, dtendMillis);
350f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
351f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        CalendarInstancesHelper.computeTimezoneDependentFields(date, dtendMillis,
352f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                eventTime, initialValues);
353f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        instancesMap.add(syncIdKey, initialValues);
354f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
355f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                } else {
356f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // the event is not repeating
357f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    initialValues = new ContentValues();
358f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
359f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // if this event has an "original" field, then record
360f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // that we need to cancel the original event (we can't
361f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // do that here because the order of this loop isn't
362f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // defined)
363f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (originalEvent != null && originalInstanceTimeMillis != -1) {
364f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // The ORIGINAL_EVENT_AND_CALENDAR holds the
365f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // calendar id concatenated with the ORIGINAL_EVENT to form
366f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // a unique key, matching the keys for instancesMap.
367f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(ORIGINAL_EVENT_AND_CALENDAR,
368f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                CalendarInstancesHelper.getSyncIdKey(originalEvent, calendarId));
369f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
370f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                originalInstanceTimeMillis);
371f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        initialValues.put(Events.STATUS, status);
372f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
373f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
374f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    long dtendMillis = dtstartMillis;
375f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (durationStr == null) {
376f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (!entries.isNull(dtendColumn)) {
377f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            dtendMillis = entries.getLong(dtendColumn);
378f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
379f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    } else {
380f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        dtendMillis = duration.addTo(dtstartMillis);
381f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
382f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
383f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // this non-recurring event might be a recurrence exception that doesn't
384f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // actually fall within our expansion window, but instead was selected
385f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // so we can correctly cancel expanded recurrence instances below.  do not
386f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // add events to the instances map if they don't actually fall within our
387f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // expansion window.
388f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if ((dtendMillis < begin) || (dtstartMillis > end)) {
389f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        if (originalEvent != null && originalInstanceTimeMillis != -1) {
390f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
391f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        } else {
392f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
393f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                Log.w(CalendarProvider2.TAG, "Unexpected event outside window: "
394f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                                        + syncId);
395f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            }
396f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            continue;
397f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        }
398f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
399f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
400f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    initialValues.put(Instances.EVENT_ID, eventId);
401f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
402f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    initialValues.put(Instances.BEGIN, dtstartMillis);
403f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    initialValues.put(Instances.END, dtendMillis);
404f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
405f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // we temporarily store the DELETED status (will be cleaned later)
406f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    initialValues.put(Events.DELETED, deleted);
407f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
408f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (allDay) {
409f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        eventTime.timezone = Time.TIMEZONE_UTC;
410f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    } else {
411f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        eventTime.timezone = localTimezone;
412f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
413f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis,
414f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                            dtendMillis, eventTime, initialValues);
415f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
416f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    instancesMap.add(syncIdKey, initialValues);
417f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
418f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            } catch (DateException e) {
419f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
420f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e);
421f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
422f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            } catch (TimeFormatException e) {
423f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (Log.isLoggable(CalendarProvider2.TAG, Log.ERROR)) {
424f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    Log.w(CalendarProvider2.TAG, "RecurrenceProcessor error ", e);
425f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
426f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
427f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
428f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
429f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Invariant: instancesMap contains all instances that affect the
430f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // window, indexed by original sync id concatenated with calendar id.
431f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // It consists of:
432f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // a) Individual events that fall in the window.  They have:
433f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   EVENT_ID, BEGIN, END
434f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // b) Instances of recurrences that fall in the window.  They may
435f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   be subject to exceptions.  They have:
436f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   EVENT_ID, BEGIN, END
437f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // c) Exceptions that fall in the window.  They have:
438f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS (since they can
439f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   be a modification or cancellation), EVENT_ID, BEGIN, END
440f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // d) Recurrence exceptions that modify an instance inside the
441f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   window but fall outside the window.  They have:
442f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   ORIGINAL_EVENT_AND_CALENDAR, ORIGINAL_INSTANCE_TIME, STATUS =
443f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   STATUS_CANCELED, EVENT_ID, BEGIN, END
444f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
445f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // First, delete the original instances corresponding to recurrence
446f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // exceptions.  We do this by iterating over the list and for each
447f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // recurrence exception, we search the list for an instance with a
448f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // matching "original instance time".  If we find such an instance,
449f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // we remove it from the list.  If we don't find such an instance
450f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // then we cancel the recurrence exception.
451f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Set<String> keys = instancesMap.keySet();
452f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        for (String syncIdKey : keys) {
453f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey);
454f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            for (ContentValues values : list) {
455f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
456f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // If this instance is not a recurrence exception, then
457f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // skip it.
458f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (!values.containsKey(ORIGINAL_EVENT_AND_CALENDAR)) {
459f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    continue;
460f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
461f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
462f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String originalEventPlusCalendar = values.getAsString(ORIGINAL_EVENT_AND_CALENDAR);
463f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
464f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                CalendarInstancesHelper.InstancesList originalList = instancesMap
465f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        .get(originalEventPlusCalendar);
466f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if (originalList == null) {
467f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    // The original recurrence is not present, so don't try canceling it.
468f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    continue;
469f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
470f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
471f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // Search the original event for a matching original
472f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // instance time.  If there is a matching one, then remove
473f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // the original one.  We do this both for exceptions that
474f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // change the original instance as well as for exceptions
475f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // that delete the original instance.
476f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                for (int num = originalList.size() - 1; num >= 0; num--) {
477f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    ContentValues originalValues = originalList.get(num);
478f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    long beginTime = originalValues.getAsLong(Instances.BEGIN);
479f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    if (beginTime == originalTime) {
480f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        // We found the original instance, so remove it.
481f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        originalList.remove(num);
482f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    }
483f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
484f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
485f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
486f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
487f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Invariant: instancesMap contains filtered instances.
488f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // It consists of:
489f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // a) Individual events that fall in the window.
490f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // b) Instances of recurrences that fall in the window and have not
491f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   been subject to exceptions.
492f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // c) Exceptions that fall in the window.  They will have
493f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   STATUS_CANCELED if they are cancellations.
494f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // d) Recurrence exceptions that modify an instance inside the
495f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        //   window but fall outside the window.  These are STATUS_CANCELED.
496f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
497f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Now do the inserts.  Since the db lock is held when this method is executed,
498f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // this will be done in a transaction.
499f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
500f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // while the calendar app is trying to query the db (expanding instances)), we will
501f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // not be "polite" and yield the lock until we're done.  This will favor local query
502f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // operations over sync/write operations.
503f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        for (String syncIdKey : keys) {
504f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            CalendarInstancesHelper.InstancesList list = instancesMap.get(syncIdKey);
505f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            for (ContentValues values : list) {
506f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
507f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // If this instance was cancelled or deleted then don't create a new
508f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // instance.
509f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                Integer status = values.getAsInteger(Events.STATUS);
510f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                boolean deleted = values.containsKey(Events.DELETED) ?
511f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                        values.getAsBoolean(Events.DELETED) : false;
512f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                if ((status != null && status == Events.STATUS_CANCELED) || deleted) {
513f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    continue;
514f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                }
515f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
516f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // We remove this useless key (not valid in the context of Instances table)
517f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                values.remove(Events.DELETED);
518f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
519f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // Remove these fields before inserting a new instance
520f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                values.remove(ORIGINAL_EVENT_AND_CALENDAR);
521f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                values.remove(Events.ORIGINAL_INSTANCE_TIME);
522f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                values.remove(Events.STATUS);
523f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
524f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                mDbHelper.instancesReplace(values);
525f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
526f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
527f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
528f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
529f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
530f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Make instances for the given range.
531f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
532f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    protected void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
533f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
534f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (CalendarProvider2.PROFILE) {
535f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Debug.startMethodTracing("expandInstanceRangeLocked");
536f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
537f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
538f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (Log.isLoggable(TAG, Log.VERBOSE)) {
539f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Log.v(TAG, "Expanding events between " + begin + " and " + end);
540f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
541f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
542f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Cursor entries = getEntries(begin, end);
543f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        try {
544f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            performInstanceExpansion(begin, end, localTimezone, entries);
545f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        } finally {
546f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (entries != null) {
547f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                entries.close();
548f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
549f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
550f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (CalendarProvider2.PROFILE) {
551f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Debug.stopMethodTracing();
552f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
553f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
554f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
555f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
556f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Get all entries affecting the given window.
557f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
558f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param begin Window start (ms).
559f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param end Window end (ms).
560f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @return Cursor for the entries; caller must close it.
561f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
562f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private Cursor getEntries(long begin, long end) {
563f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
564f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
565f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap);
566f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
567f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String beginString = String.valueOf(begin);
568f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String endString = String.valueOf(end);
569f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
570f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // grab recurrence exceptions that fall outside our expansion window but
571f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // modify
572f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // recurrences that do fall within our window. we won't insert these
573f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // into the output
574f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // set of instances, but instead will just add them to our cancellations
575f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // list, so we
576f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // can cancel the correct recurrence expansion instances.
577f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // we don't have originalInstanceDuration or end time. for now, assume
578f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // the original
579f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // instance lasts no longer than 1 week.
580f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // also filter with syncable state (we dont want the entries from a non
581f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // syncable account)
5829ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert        // also filter with last_synced=0 so we don't expand events that were
5839ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert        // dup'ed for partial updates.
584f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // TODO: compute the originalInstanceEndTime or get this from the
585f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // server.
586f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        qb.appendWhere(SQL_WHERE_GET_EVENTS_ENTRIES);
587f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String selectionArgs[] = new String[] {
5889ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                endString,
5899ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                beginString,
5909ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                endString,
5919ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                String.valueOf(begin - MAX_ASSUMED_DURATION),
5929ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                "0", // Calendars.SYNC_EVENTS
5939ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                "0", // Events.LAST_SYNCED
594f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        };
595f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Cursor c = qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs,
596f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                null /* groupBy */, null /* having */, null /* sortOrder */);
597f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (Log.isLoggable(TAG, Log.VERBOSE)) {
598f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Log.v(TAG, "Instance expansion:  got " + c.getCount() + " entries");
599f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
600f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        return c;
601f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
602f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
603f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
604f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Updates the instances table when an event is added or updated.
605f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
606f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param values The new values of the event.
607f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param rowId The database row id of the event.
608f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param newEvent true if the event is new.
609f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param db The database
610f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
611f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    public void updateInstancesLocked(ContentValues values, long rowId, boolean newEvent,
612f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            SQLiteDatabase db) {
613b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        /*
614b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * This may be a recurring event (has an RRULE or RDATE), an exception to a recurring
615b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * event (has ORIGINAL_ID or ORIGINAL_SYNC_ID), or a regular event.  Recurring events
616b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * and exceptions require additional handling.
617b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *
618b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * If this is not a new event, it may already have entries in Instances, so we want
619b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * to delete those before we do any additional work.
620b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         */
621f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
622f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // If there are no expanded Instances, then return.
623f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        MetaData.Fields fields = mMetaData.getFieldsLocked();
624f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (fields.maxInstance == 0) {
625f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            return;
626f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
627f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
628f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Long dtstartMillis = values.getAsLong(Events.DTSTART);
629f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (dtstartMillis == null) {
630f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (newEvent) {
631f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                // must be present for a new event.
632f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                throw new RuntimeException("DTSTART missing.");
633f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
634f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (Log.isLoggable(TAG, Log.VERBOSE)) {
635f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                Log.v(TAG, "Missing DTSTART.  No need to update instance.");
636f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
637f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            return;
638f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
639f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
640f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (!newEvent) {
641f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // Want to do this for regular event, recurrence, or exception.
642f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // For recurrence or exception, more deletion may happen below if we
643f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // do an instance expansion. This deletion will suffice if the
644f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // exception
645f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // is moved outside the window, for instance.
646f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            db.delete(Tables.INSTANCES, Instances.EVENT_ID + "=?", new String[] {
647f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String.valueOf(rowId)
648f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            });
649f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
650f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
651f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String rrule = values.getAsString(Events.RRULE);
652f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String rdate = values.getAsString(Events.RDATE);
653b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        String originalId = values.getAsString(Events.ORIGINAL_ID);
654b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        String originalSyncId = values.getAsString(Events.ORIGINAL_SYNC_ID);
655b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        if (CalendarProvider2.isRecurrenceEvent(rrule, rdate, originalId, originalSyncId)) {
656b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
657b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
658b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
659f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // The recurrence or exception needs to be (re-)expanded if:
660f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // a) Exception or recurrence that falls inside window
661f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            boolean insideWindow = dtstartMillis <= fields.maxInstance
662f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    && (lastDateMillis == null || lastDateMillis >= fields.minInstance);
663f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // b) Exception that affects instance inside window
664f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // These conditions match the query in getEntries
665f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // See getEntries comment for explanation of subtracting 1 week.
666f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            boolean affectsWindow = originalInstanceTime != null
667f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    && originalInstanceTime <= fields.maxInstance
668f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    && originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
669d5be35c42732d610040a64dd5bab782ef10014abAndy McFadden            if (CalendarProvider2.DEBUG_INSTANCES) {
670d5be35c42732d610040a64dd5bab782ef10014abAndy McFadden                Log.d(TAG + "-i", "Recurrence: inside=" + insideWindow +
671d5be35c42732d610040a64dd5bab782ef10014abAndy McFadden                        ", affects=" + affectsWindow);
672d5be35c42732d610040a64dd5bab782ef10014abAndy McFadden            }
673f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (insideWindow || affectsWindow) {
674f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                updateRecurrenceInstancesLocked(values, rowId, db);
675f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
676f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // TODO: an exception creation or update could be optimized by
677f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // updating just the affected instances, instead of regenerating
678f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // the recurrence.
679f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            return;
680f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
681f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
682f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Long dtendMillis = values.getAsLong(Events.DTEND);
683f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (dtendMillis == null) {
684f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            dtendMillis = dtstartMillis;
685f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
686f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
687f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // if the event is in the expanded range, insert
688f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // into the instances table.
689f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // TODO: deal with durations. currently, durations are only used in
690f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // recurrences.
691f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
692f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
693f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            ContentValues instanceValues = new ContentValues();
694f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            instanceValues.put(Instances.EVENT_ID, rowId);
695f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            instanceValues.put(Instances.BEGIN, dtstartMillis);
696f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            instanceValues.put(Instances.END, dtendMillis);
697f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
698f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            boolean allDay = false;
699f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
700f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (allDayInteger != null) {
701f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                allDay = allDayInteger != 0;
702f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
703f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
704f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            // Update the timezone-dependent fields.
705f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Time local = new Time();
706f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (allDay) {
707f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                local.timezone = Time.TIMEZONE_UTC;
708f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            } else {
709f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                local.timezone = fields.timezone;
710f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
711f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
712f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            CalendarInstancesHelper.computeTimezoneDependentFields(dtstartMillis, dtendMillis,
713f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    local, instanceValues);
714f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            mDbHelper.instancesInsert(instanceValues);
715f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
716f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
717f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
718f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
719f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Do incremental Instances update of a recurrence or recurrence exception.
720f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * This method does performInstanceExpansion on just the modified
721f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * recurrence, to avoid the overhead of recomputing the entire instance
722f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * table.
723f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
724f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param values The new values of the event.
725f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param rowId The database row id of the event.
726f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param db The database
727f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
728f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private void updateRecurrenceInstancesLocked(ContentValues values, long rowId,
729f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            SQLiteDatabase db) {
730b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        /*
731b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *  There are two categories of event that "rowId" may refer to:
732b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *  (1) Recurrence event.
733b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *  (2) Exception to recurrence event.  Has non-empty originalId (if it originated
734b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *      locally), originalSyncId (if it originated from the server), or both (if
735b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *      it's fully synchronized).
736b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *
737b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         * Exceptions may arrive from the server before the recurrence event, which means:
738b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *  - We could find an originalSyncId but a lookup on originalSyncId could fail (in
739b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *    which case we can just ignore the exception for now).
740b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *  - There may be a brief period between the time we receive a recurrence and the
741b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *    time we set originalId in related exceptions where originalSyncId is the only
742b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *    way to find exceptions for a recurrence.  Thus, an empty originalId field may
743b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         *    not be used to decide if an event is an exception.
744b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden         */
745b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
746f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        MetaData.Fields fields = mMetaData.getFieldsLocked();
747f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String instancesTimezone = mCalendarCache.readTimezoneInstances();
748b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
749b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        // Get the originalSyncId.  If it's not in "values", check the database.
750b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        String originalSyncId = values.getAsString(Events.ORIGINAL_SYNC_ID);
751b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        if (originalSyncId == null) {
752b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            originalSyncId = getEventValue(db, rowId, Events.ORIGINAL_SYNC_ID);
753b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        }
754b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
755f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String recurrenceSyncId;
756b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        if (originalSyncId != null) {
757b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // This event is an exception; set recurrenceSyncId to the original.
758b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            recurrenceSyncId = originalSyncId;
759f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        } else {
760b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // This could be a recurrence or an exception.  If it has been synced with the
761b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // server we can get the _sync_id and know for certain that it's a recurrence.
762b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // If not, we'll deal with it below.
763b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            recurrenceSyncId = values.getAsString(Events._SYNC_ID);
764b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            if (recurrenceSyncId == null) {
765b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                // Not in "values", check the database.
766b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                recurrenceSyncId = getEventValue(db, rowId, Events._SYNC_ID);
767b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            }
768f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
769f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
770b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        // Clear out old instances
771b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        int delCount;
772f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (recurrenceSyncId == null) {
773b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // We're creating or updating a recurrence or exception that hasn't been to the
774b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // server.  If this is a recurrence event, the event ID is simply the rowId.  If
775b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // it's an exception, we will find the value in the originalId field.
776b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            String originalId = values.getAsString(Events.ORIGINAL_ID);
777b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            if (originalId == null) {
778b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                // Not in "values", check the database.
779b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                originalId = getEventValue(db, rowId, Events.ORIGINAL_ID);
780b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            }
781b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            String recurrenceId;
782b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            if (originalId != null) {
783b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                // This event is an exception; set recurrenceId to the original.
784b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                recurrenceId = originalId;
785b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            } else {
786b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                // This event is a recurrence, so we just use the ID that was passed in.
787b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                recurrenceId = String.valueOf(rowId);
788b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            }
789b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
790b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // Delete Instances entries for this Event (_id == recurrenceId) and for exceptions
791b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // to this Event (originalId == recurrenceId).
792f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            String where = SQL_WHERE_ID_FROM_INSTANCES_NOT_SYNCED;
793b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            delCount = db.delete(Tables.INSTANCES, where, new String[] {
794b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden                    recurrenceId, recurrenceId
795f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            });
796f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        } else {
797b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // We're creating or updating a recurrence or exception that has been synced with
798b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // the server.  Delete Instances entries for this Event (_sync_id == recurrenceSyncId)
799b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            // and for exceptions to this Event (originalSyncId == recurrenceSyncId).
800f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            String where = SQL_WHERE_ID_FROM_INSTANCES_SYNCED;
801b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden            delCount = db.delete(Tables.INSTANCES, where, new String[] {
802f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    recurrenceSyncId, recurrenceSyncId
803f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            });
804f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
805f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
806b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        //Log.d(TAG, "Recurrence: deleted " + delCount + " instances");
807b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        //dumpInstancesTable(db);
808b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden
809f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Now do instance expansion
810b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        // TODO: passing "rowId" is wrong if this is an exception - need originalId then
811f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
812f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        try {
813f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            performInstanceExpansion(fields.minInstance, fields.maxInstance,
814f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                    instancesTimezone, entries);
815f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        } finally {
816f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            if (entries != null) {
817f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                entries.close();
818f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            }
819f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
820f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
821f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
822f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
823f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Determines the recurrence entries associated with a particular
824f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * recurrence. This set is the base recurrence and any exception. Normally
825f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * the entries are indicated by the sync id of the base recurrence (which is
826b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * the originalSyncId in the exceptions). However, a complication is that a
827f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * recurrence may not yet have a sync id. In that case, the recurrence is
828f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * specified by the rowId.
829f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
830f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param recurrenceSyncId The sync id of the base recurrence, or null.
831f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param rowId The row id of the base recurrence.
832f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @return the relevant entries.
833f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
834f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
835f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
836f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
837f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
838f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        qb.setProjectionMap(CalendarProvider2.sEventsProjectionMap);
839f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        String selectionArgs[];
840f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (recurrenceSyncId == null) {
841f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            String where = CalendarProvider2.SQL_WHERE_ID;
842f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            qb.appendWhere(where);
843f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            selectionArgs = new String[] {
844f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                String.valueOf(rowId)
845f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            };
846f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        } else {
8479ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert            // don't expand events that were dup'ed for partial updates
8489ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert            String where = "(" + Events._SYNC_ID + "=? OR " + Events.ORIGINAL_SYNC_ID + "=?) AND "
8499ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                    + Events.LAST_SYNCED + " = ?";
850f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            qb.appendWhere(where);
851f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            selectionArgs = new String[] {
8529ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                    recurrenceSyncId,
8539ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                    recurrenceSyncId,
8549ec70fada3d8f7cf56d6b0d0947823ec5bce572cAlon Albert                    "0", // Events.LAST_SYNCED
855f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            };
856f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
857f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (Log.isLoggable(TAG, Log.VERBOSE)) {
858f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Log.v(TAG, "Retrieving events to expand: " + qb.toString());
859f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
860f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
861f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs,
862f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik                null /* groupBy */, null /* having */, null /* sortOrder */);
863f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
864f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
865f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
866f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Generates a unique key from the syncId and calendarId. The purpose of
867f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * this is to prevent collisions if two different calendars use the same
868f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * sync id. This can happen if a Google calendar is accessed by two
869f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * different accounts, or with Exchange, where ids are not unique between
870f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * calendars.
871f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
872f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param syncId Id for the event
873f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param calendarId Id for the calendar
874f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @return key
875f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
876f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    static String getSyncIdKey(String syncId, long calendarId) {
877f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        return calendarId + ":" + syncId;
878f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
879f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
880f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    /**
881f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * Computes the timezone-dependent fields of an instance of an event and
882f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * updates the "values" map to contain those fields.
883f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     *
884f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param begin the start time of the instance (in UTC milliseconds)
885f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param end the end time of the instance (in UTC milliseconds)
886f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param local a Time object with the timezone set to the local timezone
887f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     * @param values a map that will contain the timezone-dependent fields
888f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik     */
889f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    static void computeTimezoneDependentFields(long begin, long end,
890f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            Time local, ContentValues values) {
891f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        local.set(begin);
892f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int startDay = Time.getJulianDay(begin, local.gmtoff);
893f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int startMinute = local.hour * 60 + local.minute;
894f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
895f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        local.set(end);
896f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int endDay = Time.getJulianDay(end, local.gmtoff);
897f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        int endMinute = local.hour * 60 + local.minute;
898f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
899f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Special case for midnight, which has endMinute == 0.  Change
900f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // that to +24 hours on the previous day to make everything simpler.
901f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // Exception: if start and end minute are both 0 on the same day,
902f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        // then leave endMinute alone.
903f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        if (endMinute == 0 && endDay > startDay) {
904f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            endMinute = 24 * 60;
905f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik            endDay -= 1;
906f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        }
907f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
908f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        values.put(Instances.START_DAY, startDay);
909f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        values.put(Instances.END_DAY, endDay);
910f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        values.put(Instances.START_MINUTE, startMinute);
911f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik        values.put(Instances.END_MINUTE, endMinute);
912f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik    }
913f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik
914b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    /**
915b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     * Dumps the contents of the Instances table to the log file.
916b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden     */
917b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    private static void dumpInstancesTable(SQLiteDatabase db) {
918b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        Cursor cursor = db.query(Tables.INSTANCES, null, null, null, null, null, null);
919b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden        DatabaseUtils.dumpCursor(cursor);
9200332925aa9db8c4826327edd85030a4791b7a8e6Michael Chan        if (cursor != null) {
9210332925aa9db8c4826327edd85030a4791b7a8e6Michael Chan            cursor.close();
9220332925aa9db8c4826327edd85030a4791b7a8e6Michael Chan        }
923b09eb917f2490a1dae20709a667df845a2e67c94Andy McFadden    }
924f5930ab5c4c0fe728cb8fdf923d482b4f272eb1fRoboErik}
925