1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.calendar;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.SystemClock;
28import android.provider.CalendarContract;
29import android.test.SyncBaseInstrumentation;
30import android.text.format.DateUtils;
31import android.text.format.Time;
32import android.util.Log;
33
34import com.google.android.collect.Maps;
35
36import java.util.HashSet;
37import java.util.Map;
38import java.util.Set;
39
40public class CalendarSyncTestingBase extends SyncBaseInstrumentation {
41    protected AccountManager mAccountManager;
42    protected Context mTargetContext;
43    protected String mAccount;
44    protected ContentResolver mResolver;
45    protected Uri mEventsUri = CalendarContract.Events.CONTENT_URI;
46
47    static final String TAG = "calendar";
48    static final String DEFAULT_TIMEZONE = "America/Los_Angeles";
49    static final Set<String> EVENT_COLUMNS_TO_SKIP = new HashSet<String>();
50    static final Set<String> ATTENDEES_COLUMNS_TO_SKIP = new HashSet<String>();
51    static final Set<String> CALENDARS_COLUMNS_TO_SKIP = new HashSet<String>();
52    static final Set<String> INSTANCES_COLUMNS_TO_SKIP = new HashSet<String>();
53
54    static {
55        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events._ID);
56        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events.SYNC_DATA5);
57        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events.SYNC_DATA4);
58        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events.SYNC_DATA2);
59        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events.DIRTY);
60        EVENT_COLUMNS_TO_SKIP.add(CalendarContract.Events.SYNC_DATA8);
61        ATTENDEES_COLUMNS_TO_SKIP.add(CalendarContract.Attendees._ID);
62        CALENDARS_COLUMNS_TO_SKIP.add(CalendarContract.Calendars._ID);
63        CALENDARS_COLUMNS_TO_SKIP.add(CalendarContract.Calendars.CAL_SYNC8);
64        CALENDARS_COLUMNS_TO_SKIP.add(CalendarContract.Calendars.CAL_SYNC7);
65        CALENDARS_COLUMNS_TO_SKIP.add(CalendarContract.Calendars.DIRTY);
66        CALENDARS_COLUMNS_TO_SKIP.add(CalendarContract.Calendars.CAL_SYNC6);
67        INSTANCES_COLUMNS_TO_SKIP.add(CalendarContract.Instances._ID);
68    }
69
70    @Override
71    protected void setUp() throws Exception {
72        super.setUp();
73        mTargetContext = getInstrumentation().getTargetContext();
74
75        mAccountManager = AccountManager.get(mTargetContext);
76        mAccount = getAccount();
77        mResolver = mTargetContext.getContentResolver();
78    }
79
80    /**
81     * A simple method that syncs the calendar provider.
82     * @throws Exception
83     */
84    protected void syncCalendar() throws Exception {
85        cancelSyncsandDisableAutoSync();
86        syncProvider(CalendarContract.CONTENT_URI, mAccount, CalendarContract.AUTHORITY);
87    }
88
89    /**
90     * Creates a new event in the default calendar.
91     * @param event Event to be created.
92     * @return Uri of the created event.
93     * @throws Exception
94     */
95    protected Uri insertEvent(EventInfo event) throws Exception {
96        return insertEvent(getDefaultCalendarId(), event);
97    }
98
99    /**
100     * Creates a new event in the given calendarId.
101     * @param calendarId Calendar to be used.
102     * @param event Event to be created.
103     * @return Uri of the event created.
104     * @throws Exception
105     */
106    protected Uri insertEvent(int calendarId, EventInfo event) throws Exception{
107        ContentValues m = new ContentValues();
108        m.put(CalendarContract.Events.CALENDAR_ID, calendarId);
109        m.put(CalendarContract.Events.TITLE, event.mTitle);
110        m.put(CalendarContract.Events.DTSTART, event.mDtstart);
111        m.put(CalendarContract.Events.ALL_DAY, event.mAllDay ? 1 : 0);
112
113        if (event.mRrule == null) {
114            // This is a normal event
115            m.put(CalendarContract.Events.DTEND, event.mDtend);
116        } else {
117            // This is a repeating event
118            m.put(CalendarContract.Events.RRULE, event.mRrule);
119            m.put(CalendarContract.Events.DURATION, event.mDuration);
120        }
121
122        if (event.mDescription != null) {
123            m.put(CalendarContract.Events.DESCRIPTION, event.mDescription);
124        }
125        if (event.mTimezone != null) {
126            m.put(CalendarContract.Events.EVENT_TIMEZONE, event.mTimezone);
127        }
128
129        Uri url = mResolver.insert(mEventsUri, m);
130        syncCalendar();
131        return url;
132    }
133
134    /**
135     * Edits the given event.
136     * @param eventId EventID of the event to be edited.
137     * @param event Edited event details.
138     * @throws Exception
139     */
140    protected void editEvent(long eventId, EventInfo event) throws Exception {
141        ContentValues values = new ContentValues();
142        values.put(CalendarContract.Events.TITLE, event.mTitle);
143        values.put(CalendarContract.Events.DTSTART, event.mDtstart);
144        values.put(CalendarContract.Events.DTEND, event.mDtend);
145        values.put(CalendarContract.Events.ALL_DAY, event.mAllDay ? 1 : 0);
146
147        if (event.mDescription != null) {
148            values.put(CalendarContract.Events.DESCRIPTION, event.mDescription);
149        }
150
151        Uri uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId);
152        mResolver.update(uri, values, null, null);
153        syncCalendar();
154    }
155
156    /**
157     * Deletes a given event.
158     * @param uri
159     * @throws Exception
160     */
161    protected void deleteEvent(Uri uri) throws Exception {
162        mResolver.delete(uri, null, null);
163        syncCalendar();
164    }
165
166    /**
167     * Inserts a new calendar.
168     * @param name
169     * @param timezone
170     * @param calendarUrl
171     * @throws Exception
172     */
173    protected void insertCalendar(String name, String timezone, String calendarUrl)
174            throws Exception {
175        ContentValues values = new ContentValues();
176
177        values.put(CalendarContract.Calendars.ACCOUNT_NAME, getAccount());
178        values.put(CalendarContract.Calendars.CAL_SYNC1, calendarUrl);
179        values.put(CalendarContract.Calendars.NAME, name);
180        values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, name);
181
182        values.put(CalendarContract.Calendars.SYNC_EVENTS, 1);
183        values.put(CalendarContract.Calendars.VISIBLE, 1);
184        values.put(CalendarContract.Calendars.CALENDAR_COLOR, -14069085 /* blue */);
185        values.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL,
186                CalendarContract.Calendars.CAL_ACCESS_OWNER);
187
188        values.put(CalendarContract.Calendars.CALENDAR_COLOR, "0xff123456");
189        values.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, timezone);
190        mResolver.insert(CalendarContract.Calendars.CONTENT_URI, values);
191        syncCalendar();
192    }
193
194    /**
195     * Returns a fresh count of events.
196     * @return
197     */
198    protected int getEventsCount() {
199        Cursor cursor;
200        cursor = mResolver.query(mEventsUri, null, null, null, null);
201        return cursor.getCount();
202    }
203
204    /**
205     * Returns the ID of the default calendar.
206     * @return
207     */
208    protected int getDefaultCalendarId() {
209        Cursor calendarsCursor;
210        calendarsCursor = mResolver.query(CalendarContract.Calendars.CONTENT_URI, null, null, null,
211                null);
212        calendarsCursor.moveToNext();
213        return calendarsCursor.getInt(calendarsCursor.getColumnIndex("_id"));
214    }
215
216    /**
217     * This class stores all the useful information about an event.
218     */
219    protected class EventInfo {
220        String mTitle;
221        String mDescription;
222        String mTimezone;
223        boolean mAllDay;
224        long mDtstart;
225        long mDtend;
226        String mRrule;
227        String mDuration;
228        String mOriginalTitle;
229        long mOriginalInstance;
230        int mSyncId;
231
232        // Constructor for normal events, using the default timezone
233        public EventInfo(String title, String startDate, String endDate,
234                boolean allDay) {
235            init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE);
236        }
237
238        public EventInfo(String title, long startDate, long endDate,
239                boolean allDay) {
240            mTitle = title;
241            mTimezone = DEFAULT_TIMEZONE;
242            mDtstart = startDate;
243            mDtend = endDate;
244            mDuration = null;
245            mRrule = null;
246            mAllDay = allDay;
247        }
248
249        public EventInfo(String title, long startDate, long endDate,
250                boolean allDay, String description) {
251            mTitle = title;
252            mTimezone = DEFAULT_TIMEZONE;
253            mDtstart = startDate;
254            mDtend = endDate;
255            mDuration = null;
256            mRrule = null;
257            mAllDay = allDay;
258            mDescription = description;
259        }
260
261        // Constructor for normal events, specifying the timezone
262        public EventInfo(String title, String startDate, String endDate,
263                boolean allDay, String timezone) {
264            init(title, startDate, endDate, allDay, timezone);
265        }
266
267        public void init(String title, String startDate, String endDate,
268                boolean allDay, String timezone) {
269            mTitle = title;
270            Time time = new Time();
271            if (allDay) {
272                time.timezone = Time.TIMEZONE_UTC;
273            } else if (timezone != null) {
274                time.timezone = timezone;
275            }
276            mTimezone = time.timezone;
277            time.parse3339(startDate);
278            mDtstart = time.toMillis(false /* use isDst */);
279            time.parse3339(endDate);
280            mDtend = time.toMillis(false /* use isDst */);
281            mDuration = null;
282            mRrule = null;
283            mAllDay = allDay;
284        }
285
286        // Constructor for repeating events, using the default timezone
287        public EventInfo(String title, String description, String startDate, String endDate,
288                String rrule, boolean allDay) {
289            init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE);
290        }
291
292        // Constructor for repeating events, specifying the timezone
293        public EventInfo(String title, String description, String startDate, String endDate,
294                String rrule, boolean allDay, String timezone) {
295            init(title, description, startDate, endDate, rrule, allDay, timezone);
296        }
297
298        public void init(String title, String description, String startDate, String endDate,
299                String rrule, boolean allDay, String timezone) {
300            mTitle = title;
301            mDescription = description;
302            Time time = new Time();
303            if (allDay) {
304                time.timezone = Time.TIMEZONE_UTC;
305            } else if (timezone != null) {
306                time.timezone = timezone;
307            }
308            mTimezone = time.timezone;
309            time.parse3339(startDate);
310            mDtstart = time.toMillis(false /* use isDst */);
311            if (endDate != null) {
312                time.parse3339(endDate);
313                mDtend = time.toMillis(false /* use isDst */);
314            }
315            if (allDay) {
316                long days = 1;
317                if (endDate != null) {
318                    days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS;
319                }
320                mDuration = "P" + days + "D";
321            } else {
322                long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS;
323                mDuration = "P" + seconds + "S";
324            }
325            mRrule = rrule;
326            mAllDay = allDay;
327        }
328
329        // Constructor for recurrence exceptions, using the default timezone
330        public EventInfo(String originalTitle, String originalInstance, String title,
331                String description, String startDate, String endDate, boolean allDay) {
332            init(originalTitle, originalInstance,
333                    title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE);
334        }
335
336        public void init(String originalTitle, String originalInstance,
337                String title, String description, String startDate, String endDate,
338                boolean allDay, String timezone) {
339            mOriginalTitle = originalTitle;
340            Time time = new Time(timezone);
341            time.parse3339(originalInstance);
342            mOriginalInstance = time.toMillis(false /* use isDst */);
343            init(title, description, startDate, endDate, null /* rrule */, allDay, timezone);
344        }
345    }
346
347    /**
348     * Returns the default account on the device.
349     * @return
350     */
351    protected String getAccount() {
352        Account[] accounts = mAccountManager.getAccountsByType("com.google");
353
354        assertTrue("Didn't find any Google accounts", accounts.length > 0);
355
356        Account account = accounts[accounts.length - 1];
357        Log.v(TAG, "Found " + accounts.length + " accounts; using the last one, " + account.name);
358        return account.name;
359    }
360
361    /**
362     * Compares two cursors
363     */
364    protected void compareCursors(Cursor cursor1, Cursor cursor2,
365                                  Set<String> columnsToSkip, String tableName) {
366        String[] cols = cursor1.getColumnNames();
367        int length = cols.length;
368
369        assertEquals(tableName + " count failed to match", cursor1.getCount(),
370                cursor2.getCount());
371        Map<String, String> row = Maps.newHashMap();
372        while (cursor1.moveToNext() && cursor2.moveToNext()) {
373            for (int i = 0; i < length; i++) {
374                String col = cols[i];
375                if (columnsToSkip != null && columnsToSkip.contains(col)) {
376                    continue;
377                }
378                row.put(col, cursor1.getString(i));
379
380                assertEquals("Row: " + row + " Table: " + tableName + ": " + cols[i] +
381                        " failed to match", cursor1.getString(i),
382                        cursor2.getString(i));
383            }
384        }
385    }
386}
387