| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.providers.calendar; |
| |
| import android.content.ComponentName; |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ProviderInfo; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.net.Uri; |
| import android.os.UserHandle; |
| import android.provider.BaseColumns; |
| import android.provider.CalendarContract; |
| import android.provider.CalendarContract.Calendars; |
| import android.provider.CalendarContract.Colors; |
| import android.provider.CalendarContract.Events; |
| import android.provider.CalendarContract.Instances; |
| import android.test.AndroidTestCase; |
| import android.test.IsolatedContext; |
| import android.test.RenamingDelegatingContext; |
| import android.test.mock.MockContentResolver; |
| import android.test.mock.MockContext; |
| import android.test.suitebuilder.annotation.SmallTest; |
| import android.test.suitebuilder.annotation.Smoke; |
| import android.test.suitebuilder.annotation.Suppress; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.text.format.Time; |
| import android.util.Log; |
| |
| import com.android.providers.calendar.enterprise.CrossProfileCalendarHelper; |
| |
| import java.io.File; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TimeZone; |
| |
| /** |
| * Runs various tests on an isolated Calendar provider with its own database. |
| * |
| * You can run the tests with the following command line: |
| * |
| * adb shell am instrument |
| * -e debug false |
| * -w |
| * -e class com.android.providers.calendar.CalendarProvider2Test |
| * com.android.providers.calendar.tests/android.test.InstrumentationTestRunner |
| * |
| * This test no longer extends ProviderTestCase2 because it actually doesn't |
| * allow you to inject a custom context (which we needed to mock out the calls |
| * to start a service). We the next best thing, which is copy the relevant code |
| * from PTC2 and extend AndroidTestCase instead. |
| */ |
| // flaky test, add back to LargeTest when fixed - bug 2395696 |
| // @LargeTest |
| public class CalendarProvider2Test extends AndroidTestCase { |
| static final String TAG = "calendar"; |
| |
| private static final String DEFAULT_ACCOUNT_TYPE = "com.google"; |
| private static final String DEFAULT_ACCOUNT = "joe@joe.com"; |
| |
| |
| private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=?"; |
| private static final String[] WHERE_CALENDARS_ARGS = { |
| "1" |
| }; |
| private static final String WHERE_COLOR_ACCOUNT_AND_INDEX = Colors.ACCOUNT_NAME + "=? AND " |
| + Colors.ACCOUNT_TYPE + "=? AND " + Colors.COLOR_KEY + "=?"; |
| private static final String DEFAULT_SORT_ORDER = "begin ASC"; |
| |
| private CalendarProvider2ForTesting mProvider; |
| private CalendarProvider2ForTesting mWorkProfileProvider; |
| |
| private SQLiteDatabase mDb; |
| private MetaData mMetaData; |
| private Context mContext; |
| private Context mWorkContext; |
| private MockContentResolver mResolver; |
| private Uri mEventsUri = Events.CONTENT_URI; |
| private Uri mCalendarsUri = Calendars.CONTENT_URI; |
| private int mCalendarId; |
| |
| protected boolean mWipe = false; |
| protected boolean mForceDtend = false; |
| |
| // We need a unique id to put in the _sync_id field so that we can create |
| // recurrence exceptions that refer to recurring events. |
| private int mGlobalSyncId = 1000; |
| private static final String CALENDAR_URL = |
| "http://www.google.com/calendar/feeds/joe%40joe.com/private/full"; |
| |
| private static final String TIME_ZONE_AMERICA_ANCHORAGE = "America/Anchorage"; |
| private static final String TIME_ZONE_AMERICA_LOS_ANGELES = "America/Los_Angeles"; |
| private static final String DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES; |
| |
| private static final String MOCK_TIME_ZONE_DATABASE_VERSION = "2010a"; |
| |
| private static final long ONE_MINUTE_MILLIS = 60*1000; |
| private static final long ONE_HOUR_MILLIS = 3600*1000; |
| private static final long ONE_WEEK_MILLIS = 7 * 24 * 3600 * 1000; |
| |
| private static final int WORK_PROFILE_USER_ID = 10; |
| private static final String WORK_PROFILE_AUTHORITY = String.format("%d@%s", |
| WORK_PROFILE_USER_ID, CalendarContract.AUTHORITY); |
| |
| private static long parseTimeStringToMillis(String timeStr, String timeZone) { |
| Time time = new Time(timeZone); |
| time.parse3339(timeStr); |
| return time.toMillis(false /* use isDst */); |
| } |
| |
| private static String WORK_DEFAULT_TIMEZONE = TIME_ZONE_AMERICA_LOS_ANGELES; |
| |
| private static String WORK_CALENDAR_TITLE = "Calendar1"; |
| private static String WORK_CALENDAR_TITLE_STANDBY = "Calendar2"; |
| private static int WORK_CALENDAR_COLOR = 0xFFFF0000; |
| |
| private static String WORK_EVENT_TITLE = "event_title1"; |
| private static String WORK_EVENT_TITLE_STANDBY = "event_title2"; |
| private static long WORK_EVENT_DTSTART = parseTimeStringToMillis( |
| "2018-05-01T00:00:00", WORK_DEFAULT_TIMEZONE); |
| private static long WORK_EVENT_DTEND = parseTimeStringToMillis( |
| "2018-05-01T20:00:00", WORK_DEFAULT_TIMEZONE); |
| private final long WORK_EVENT_DTSTART_STANDBY = parseTimeStringToMillis( |
| "2008-05-01T00:00:00", WORK_DEFAULT_TIMEZONE); |
| private final long WORK_EVENT_DTEND_STANDBY = parseTimeStringToMillis( |
| "2008-05-01T20:00:00", WORK_DEFAULT_TIMEZONE); |
| private static int WORK_EVENT_COLOR = 0xff123456; |
| private static String WORK_EVENT_LOCATION = "Work event location."; |
| private static String WORK_EVENT_DESCRIPTION = "This is a work event."; |
| |
| /** |
| * We need a few more stub methods so that our tests can run |
| */ |
| protected class MockContext2 extends MockContext { |
| |
| @Override |
| public String getPackageName() { |
| return getContext().getPackageName(); |
| } |
| |
| @Override |
| public Resources getResources() { |
| return getContext().getResources(); |
| } |
| |
| @Override |
| public File getDir(String name, int mode) { |
| // name the directory so the directory will be seperated from |
| // one created through the regular Context |
| return getContext().getDir("mockcontext2_" + name, mode); |
| } |
| |
| @Override |
| public ComponentName startService(Intent service) { |
| return null; |
| } |
| |
| @Override |
| public boolean stopService(Intent service) { |
| return false; |
| } |
| |
| @Override |
| public PackageManager getPackageManager() { |
| return getContext().getPackageManager(); |
| } |
| } |
| |
| /** |
| * KeyValue is a simple class that stores a pair of strings representing |
| * a (key, value) pair. This is used for updating events. |
| */ |
| private class KeyValue { |
| String key; |
| String value; |
| |
| public KeyValue(String key, String value) { |
| this.key = key; |
| this.value = value; |
| } |
| } |
| |
| /** |
| * A generic command interface. This is used to support a sequence of |
| * commands that can create events, delete or update events, and then |
| * check that the state of the database is as expected. |
| */ |
| private interface Command { |
| public void execute(); |
| } |
| |
| /** |
| * This is used to insert a new event into the database. The event is |
| * specified by its name (or "title"). All of the event fields (the |
| * start and end time, whether it is an all-day event, and so on) are |
| * stored in a separate table (the "mEvents" table). |
| */ |
| private class Insert implements Command { |
| EventInfo eventInfo; |
| |
| public Insert(String eventName) { |
| eventInfo = findEvent(eventName); |
| } |
| |
| public void execute() { |
| Log.i(TAG, "insert " + eventInfo.mTitle); |
| insertEvent(mCalendarId, eventInfo); |
| } |
| } |
| |
| /** |
| * This is used to delete an event, specified by the event name. |
| */ |
| private class Delete implements Command { |
| String eventName; |
| String account; |
| String accountType; |
| int expected; |
| |
| public Delete(String eventName, int expected, String account, String accountType) { |
| this.eventName = eventName; |
| this.expected = expected; |
| this.account = account; |
| this.accountType = accountType; |
| } |
| |
| public void execute() { |
| Log.i(TAG, "delete " + eventName); |
| int rows = deleteMatchingEvents(eventName, account, accountType); |
| assertEquals(expected, rows); |
| } |
| } |
| |
| /** |
| * This is used to update an event. The values to update are specified |
| * with an array of (key, value) pairs. Both the key and value are |
| * specified as strings. Event fields that are not really strings (such |
| * as DTSTART which is a long) should be converted to the appropriate type |
| * but that isn't supported yet. When needed, that can be added here |
| * by checking for specific keys and converting the associated values. |
| */ |
| private class Update implements Command { |
| String eventName; |
| KeyValue[] pairs; |
| |
| public Update(String eventName, KeyValue[] pairs) { |
| this.eventName = eventName; |
| this.pairs = pairs; |
| } |
| |
| public void execute() { |
| Log.i(TAG, "update " + eventName); |
| if (mWipe) { |
| // Wipe instance table so it will be regenerated |
| mMetaData.clearInstanceRange(); |
| } |
| ContentValues map = new ContentValues(); |
| for (KeyValue pair : pairs) { |
| String value = pair.value; |
| if (CalendarContract.Events.STATUS.equals(pair.key)) { |
| // Do type conversion for STATUS |
| map.put(pair.key, Integer.parseInt(value)); |
| } else { |
| map.put(pair.key, value); |
| } |
| } |
| if (map.size() == 1 && map.containsKey(Events.STATUS)) { |
| updateMatchingEventsStatusOnly(eventName, map); |
| } else { |
| updateMatchingEvents(eventName, map); |
| } |
| } |
| } |
| |
| /** |
| * This command queries the number of events and compares it to the given |
| * expected value. |
| */ |
| private class QueryNumEvents implements Command { |
| int expected; |
| |
| public QueryNumEvents(int expected) { |
| this.expected = expected; |
| } |
| |
| public void execute() { |
| Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(expected, cursor.getCount()); |
| cursor.close(); |
| } |
| } |
| |
| |
| /** |
| * This command dumps the list of events to the log for debugging. |
| */ |
| private class DumpEvents implements Command { |
| |
| public DumpEvents() { |
| } |
| |
| public void execute() { |
| Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); |
| dumpCursor(cursor); |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * This command dumps the list of instances to the log for debugging. |
| */ |
| private class DumpInstances implements Command { |
| long begin; |
| long end; |
| |
| public DumpInstances(String startDate, String endDate) { |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(startDate); |
| begin = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| end = time.toMillis(false /* use isDst */); |
| } |
| |
| public void execute() { |
| Cursor cursor = queryInstances(begin, end); |
| dumpCursor(cursor); |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * This command queries the number of instances and compares it to the given |
| * expected value. |
| */ |
| private class QueryNumInstances implements Command { |
| int expected; |
| long begin; |
| long end; |
| |
| public QueryNumInstances(String startDate, String endDate, int expected) { |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(startDate); |
| begin = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| end = time.toMillis(false /* use isDst */); |
| this.expected = expected; |
| } |
| |
| public void execute() { |
| Cursor cursor = queryInstances(begin, end); |
| assertEquals(expected, cursor.getCount()); |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * When this command runs it verifies that all of the instances in the |
| * given range match the expected instances (each instance is specified by |
| * a start date). |
| * If you just want to verify that an instance exists in a given date |
| * range, use {@link VerifyInstance} instead. |
| */ |
| private class VerifyAllInstances implements Command { |
| long[] instances; |
| long begin; |
| long end; |
| |
| public VerifyAllInstances(String startDate, String endDate, String[] dates) { |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(startDate); |
| begin = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| end = time.toMillis(false /* use isDst */); |
| |
| if (dates == null) { |
| return; |
| } |
| |
| // Convert all the instance date strings to UTC milliseconds |
| int len = dates.length; |
| this.instances = new long[len]; |
| int index = 0; |
| for (String instance : dates) { |
| time.parse3339(instance); |
| this.instances[index++] = time.toMillis(false /* use isDst */); |
| } |
| } |
| |
| public void execute() { |
| Cursor cursor = queryInstances(begin, end); |
| int len = 0; |
| if (instances != null) { |
| len = instances.length; |
| } |
| if (len != cursor.getCount()) { |
| dumpCursor(cursor); |
| } |
| assertEquals("number of instances don't match", len, cursor.getCount()); |
| |
| if (instances == null) { |
| return; |
| } |
| |
| int beginColumn = cursor.getColumnIndex(Instances.BEGIN); |
| while (cursor.moveToNext()) { |
| long begin = cursor.getLong(beginColumn); |
| |
| // Search the list of expected instances for a matching start |
| // time. |
| boolean found = false; |
| for (long instance : instances) { |
| if (instance == begin) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| int titleColumn = cursor.getColumnIndex(Events.TITLE); |
| int allDayColumn = cursor.getColumnIndex(Events.ALL_DAY); |
| |
| String title = cursor.getString(titleColumn); |
| boolean allDay = cursor.getInt(allDayColumn) != 0; |
| int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | |
| DateUtils.FORMAT_24HOUR; |
| if (allDay) { |
| flags |= DateUtils.FORMAT_UTC; |
| } else { |
| flags |= DateUtils.FORMAT_SHOW_TIME; |
| } |
| String date = DateUtils.formatDateRange(mContext, begin, begin, flags); |
| String mesg = String.format("Test failed!" |
| + " unexpected instance (\"%s\") at %s", |
| title, date); |
| Log.e(TAG, mesg); |
| } |
| if (!found) { |
| dumpCursor(cursor); |
| } |
| assertTrue(found); |
| } |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * When this command runs it verifies that the given instance exists in |
| * the given date range. |
| */ |
| private class VerifyInstance implements Command { |
| long instance; |
| boolean allDay; |
| long begin; |
| long end; |
| |
| /** |
| * Creates a command to check that the given range [startDate,endDate] |
| * contains a specific instance of an event (specified by "date"). |
| * |
| * @param startDate the beginning of the date range |
| * @param endDate the end of the date range |
| * @param date the date or date-time string of an event instance |
| */ |
| public VerifyInstance(String startDate, String endDate, String date) { |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(startDate); |
| begin = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| end = time.toMillis(false /* use isDst */); |
| |
| // Convert the instance date string to UTC milliseconds |
| time.parse3339(date); |
| allDay = time.allDay; |
| instance = time.toMillis(false /* use isDst */); |
| } |
| |
| public void execute() { |
| Cursor cursor = queryInstances(begin, end); |
| int beginColumn = cursor.getColumnIndex(Instances.BEGIN); |
| boolean found = false; |
| while (cursor.moveToNext()) { |
| long begin = cursor.getLong(beginColumn); |
| |
| if (instance == begin) { |
| found = true; |
| break; |
| } |
| } |
| if (!found) { |
| int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE; |
| if (allDay) { |
| flags |= DateUtils.FORMAT_UTC; |
| } else { |
| flags |= DateUtils.FORMAT_SHOW_TIME; |
| } |
| String date = DateUtils.formatDateRange(mContext, instance, instance, flags); |
| String mesg = String.format("Test failed!" |
| + " cannot find instance at %s", |
| date); |
| Log.e(TAG, mesg); |
| } |
| assertTrue(found); |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * This class stores all the useful information about an event. |
| */ |
| private class EventInfo { |
| String mTitle; |
| String mDescription; |
| String mTimezone; |
| boolean mAllDay; |
| long mDtstart; |
| long mDtend; |
| String mRrule; |
| String mDuration; |
| String mOriginalTitle; |
| long mOriginalInstance; |
| int mSyncId; |
| String mCustomAppPackage; |
| String mCustomAppUri; |
| String mUid2445; |
| |
| // Constructor for normal events, using the default timezone |
| public EventInfo(String title, String startDate, String endDate, |
| boolean allDay) { |
| init(title, startDate, endDate, allDay, DEFAULT_TIMEZONE); |
| } |
| |
| // Constructor for normal events, specifying the timezone |
| public EventInfo(String title, String startDate, String endDate, |
| boolean allDay, String timezone) { |
| init(title, startDate, endDate, allDay, timezone); |
| } |
| |
| public void init(String title, String startDate, String endDate, |
| boolean allDay, String timezone) { |
| mTitle = title; |
| Time time = new Time(); |
| if (allDay) { |
| time.timezone = Time.TIMEZONE_UTC; |
| } else if (timezone != null) { |
| time.timezone = timezone; |
| } |
| mTimezone = time.timezone; |
| time.parse3339(startDate); |
| mDtstart = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| mDtend = time.toMillis(false /* use isDst */); |
| mDuration = null; |
| mRrule = null; |
| mAllDay = allDay; |
| mCustomAppPackage = "CustomAppPackage-" + mTitle; |
| mCustomAppUri = "CustomAppUri-" + mTitle; |
| mUid2445 = null; |
| } |
| |
| // Constructor for repeating events, using the default timezone |
| public EventInfo(String title, String description, String startDate, String endDate, |
| String rrule, boolean allDay) { |
| init(title, description, startDate, endDate, rrule, allDay, DEFAULT_TIMEZONE); |
| } |
| |
| // Constructor for repeating events, specifying the timezone |
| public EventInfo(String title, String description, String startDate, String endDate, |
| String rrule, boolean allDay, String timezone) { |
| init(title, description, startDate, endDate, rrule, allDay, timezone); |
| } |
| |
| public void init(String title, String description, String startDate, String endDate, |
| String rrule, boolean allDay, String timezone) { |
| mTitle = title; |
| mDescription = description; |
| Time time = new Time(); |
| if (allDay) { |
| time.timezone = Time.TIMEZONE_UTC; |
| } else if (timezone != null) { |
| time.timezone = timezone; |
| } |
| mTimezone = time.timezone; |
| time.parse3339(startDate); |
| mDtstart = time.toMillis(false /* use isDst */); |
| if (endDate != null) { |
| time.parse3339(endDate); |
| mDtend = time.toMillis(false /* use isDst */); |
| } |
| if (allDay) { |
| long days = 1; |
| if (endDate != null) { |
| days = (mDtend - mDtstart) / DateUtils.DAY_IN_MILLIS; |
| } |
| mDuration = "P" + days + "D"; |
| } else { |
| long seconds = (mDtend - mDtstart) / DateUtils.SECOND_IN_MILLIS; |
| mDuration = "P" + seconds + "S"; |
| } |
| mRrule = rrule; |
| mAllDay = allDay; |
| } |
| |
| // Constructor for recurrence exceptions, using the default timezone |
| public EventInfo(String originalTitle, String originalInstance, String title, |
| String description, String startDate, String endDate, boolean allDay, |
| String customPackageName, String customPackageUri, String mUid2445) { |
| init(originalTitle, originalInstance, |
| title, description, startDate, endDate, allDay, DEFAULT_TIMEZONE, |
| customPackageName, customPackageUri, mUid2445); |
| } |
| |
| public void init(String originalTitle, String originalInstance, |
| String title, String description, String startDate, String endDate, |
| boolean allDay, String timezone, String customPackageName, |
| String customPackageUri, String uid2445) { |
| mOriginalTitle = originalTitle; |
| Time time = new Time(timezone); |
| time.parse3339(originalInstance); |
| mOriginalInstance = time.toMillis(false /* use isDst */); |
| mCustomAppPackage = customPackageName; |
| mCustomAppUri = customPackageUri; |
| mUid2445 = uid2445; |
| init(title, description, startDate, endDate, null /* rrule */, allDay, timezone); |
| } |
| } |
| |
| private class InstanceInfo { |
| EventInfo mEvent; |
| long mBegin; |
| long mEnd; |
| int mExpectedOccurrences; |
| |
| public InstanceInfo(String eventName, String startDate, String endDate, int expected) { |
| // Find the test index that contains the given event name |
| mEvent = findEvent(eventName); |
| Time time = new Time(mEvent.mTimezone); |
| time.parse3339(startDate); |
| mBegin = time.toMillis(false /* use isDst */); |
| time.parse3339(endDate); |
| mEnd = time.toMillis(false /* use isDst */); |
| mExpectedOccurrences = expected; |
| } |
| } |
| |
| /** |
| * This is the main table of events. The events in this table are |
| * referred to by name in other places. |
| */ |
| private EventInfo[] mEvents = { |
| new EventInfo("normal0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", false), |
| new EventInfo("normal1", "2008-05-26T08:30:00", "2008-05-26T09:30:00", false), |
| new EventInfo("normal2", "2008-05-26T14:30:00", "2008-05-26T15:30:00", false), |
| new EventInfo("allday0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", true), |
| new EventInfo("allday1", "2008-05-02T00:00:00", "2008-05-31T00:00:00", true), |
| new EventInfo("daily0", "daily from 5/1/2008 12am to 1am", |
| "2008-05-01T00:00:00", "2008-05-01T01:00:00", |
| "FREQ=DAILY;WKST=SU", false), |
| new EventInfo("daily1", "daily from 5/1/2008 8:30am to 9:30am until 5/3/2008 8am", |
| "2008-05-01T08:30:00", "2008-05-01T09:30:00", |
| "FREQ=DAILY;UNTIL=20080503T150000Z;WKST=SU", false), |
| new EventInfo("daily2", "daily from 5/1/2008 8:45am to 9:15am until 5/3/2008 10am", |
| "2008-05-01T08:45:00", "2008-05-01T09:15:00", |
| "FREQ=DAILY;UNTIL=20080503T170000Z;WKST=SU", false), |
| new EventInfo("allday daily0", "all-day daily from 5/1/2008", |
| "2008-05-01", null, |
| "FREQ=DAILY;WKST=SU", true), |
| new EventInfo("allday daily1", "all-day daily from 5/1/2008 until 5/3/2008", |
| "2008-05-01", null, |
| "FREQ=DAILY;UNTIL=20080503T000000Z;WKST=SU", true), |
| new EventInfo("allday weekly0", "all-day weekly from 5/1/2008", |
| "2008-05-01", null, |
| "FREQ=WEEKLY;WKST=SU", true), |
| new EventInfo("allday weekly1", "all-day for 2 days weekly from 5/1/2008", |
| "2008-05-01", "2008-05-03", |
| "FREQ=WEEKLY;WKST=SU", true), |
| new EventInfo("allday yearly0", "all-day yearly on 5/1/2008", |
| "2008-05-01T", null, |
| "FREQ=YEARLY;WKST=SU", true), |
| new EventInfo("weekly0", "weekly from 5/6/2008 on Tue 1pm to 2pm", |
| "2008-05-06T13:00:00", "2008-05-06T14:00:00", |
| "FREQ=WEEKLY;BYDAY=TU;WKST=MO", false), |
| new EventInfo("weekly1", "every 2 weeks from 5/6/2008 on Tue from 2:30pm to 3:30pm", |
| "2008-05-06T14:30:00", "2008-05-06T15:30:00", |
| "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false), |
| new EventInfo("monthly0", "monthly from 5/20/2008 on the 3rd Tues from 3pm to 4pm", |
| "2008-05-20T15:00:00", "2008-05-20T16:00:00", |
| "FREQ=MONTHLY;BYDAY=3TU;WKST=SU", false), |
| new EventInfo("monthly1", "monthly from 5/1/2008 on the 1st from 12:00am to 12:10am", |
| "2008-05-01T00:00:00", "2008-05-01T00:10:00", |
| "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=1", false), |
| new EventInfo("monthly2", "monthly from 5/31/2008 on the 31st 11pm to midnight", |
| "2008-05-31T23:00:00", "2008-06-01T00:00:00", |
| "FREQ=MONTHLY;WKST=SU;BYMONTHDAY=31", false), |
| new EventInfo("daily0", "2008-05-01T00:00:00", |
| "except0", "daily0 exception for 5/1/2008 12am, change to 5/1/2008 2am to 3am", |
| "2008-05-01T02:00:00", "2008-05-01T01:03:00", false, "AppPkg1", "AppUri1", |
| "uid2445-1"), |
| new EventInfo("daily0", "2008-05-03T00:00:00", |
| "except1", "daily0 exception for 5/3/2008 12am, change to 5/3/2008 2am to 3am", |
| "2008-05-03T02:00:00", "2008-05-03T01:03:00", false, "AppPkg2", "AppUri2", |
| null), |
| new EventInfo("daily0", "2008-05-02T00:00:00", |
| "except2", "daily0 exception for 5/2/2008 12am, change to 1/2/2008", |
| "2008-01-02T00:00:00", "2008-01-02T01:00:00", false, "AppPkg3", "AppUri3", |
| "12345@uid2445"), |
| new EventInfo("weekly0", "2008-05-13T13:00:00", |
| "except3", "daily0 exception for 5/11/2008 1pm, change to 12/11/2008 1pm", |
| "2008-12-11T13:00:00", "2008-12-11T14:00:00", false, "AppPkg4", "AppUri4", |
| null), |
| new EventInfo("weekly0", "2008-05-13T13:00:00", |
| "cancel0", "weekly0 exception for 5/13/2008 1pm", |
| "2008-05-13T13:00:00", "2008-05-13T14:00:00", false, "AppPkg5", "AppUri5", |
| null), |
| new EventInfo("yearly0", "yearly on 5/1/2008 from 1pm to 2pm", |
| "2008-05-01T13:00:00", "2008-05-01T14:00:00", |
| "FREQ=YEARLY;WKST=SU", false), |
| }; |
| |
| /** |
| * This table is used to verify the events generated by mEvents. It checks that the |
| * number of instances within a given range matches the expected number |
| * of instances. |
| */ |
| private InstanceInfo[] mInstanceRanges = { |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T00:01:00", 1), |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 2), |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-02T23:59:00", 2), |
| new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), |
| new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-02T01:00:00", 1), |
| new InstanceInfo("daily0", "2008-05-02T00:00:00", "2008-05-03T00:00:00", 2), |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 31), |
| new InstanceInfo("daily0", "2008-05-01T00:00:00", "2008-06-01T23:59:00", 32), |
| |
| new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), |
| new InstanceInfo("daily1", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 2), |
| |
| new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 1), |
| new InstanceInfo("daily2", "2008-05-01T00:00:00", "2008-05-31T23:59:00", 3), |
| |
| new InstanceInfo("allday daily0", "2008-05-01", "2008-05-07", 7), |
| new InstanceInfo("allday daily1", "2008-05-01", "2008-05-07", 3), |
| new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-07", 1), |
| new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-08", 2), |
| new InstanceInfo("allday weekly0", "2008-05-01", "2008-05-31", 5), |
| new InstanceInfo("allday weekly1", "2008-05-01", "2008-05-31", 5), |
| new InstanceInfo("allday yearly0", "2008-05-01", "2009-04-30", 1), |
| new InstanceInfo("allday yearly0", "2008-05-01", "2009-05-02", 2), |
| |
| new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), |
| new InstanceInfo("weekly0", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), |
| new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 4), |
| new InstanceInfo("weekly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 8), |
| |
| new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-02T00:00:00", 0), |
| new InstanceInfo("weekly1", "2008-05-06T00:00:00", "2008-05-07T00:00:00", 1), |
| new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 2), |
| new InstanceInfo("weekly1", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 4), |
| |
| new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T13:00:00", 0), |
| new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-20T15:00:00", 1), |
| new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-05-31T00:00:00", 0), |
| new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T14:59:00", 0), |
| new InstanceInfo("monthly0", "2008-05-20T16:01:00", "2008-06-17T15:00:00", 1), |
| new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), |
| new InstanceInfo("monthly0", "2008-05-01T00:00:00", "2008-06-30T00:00:00", 2), |
| |
| new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-01T01:00:00", 1), |
| new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 1), |
| new InstanceInfo("monthly1", "2008-05-01T00:10:00", "2008-05-31T23:59:00", 1), |
| new InstanceInfo("monthly1", "2008-05-01T00:11:00", "2008-05-31T23:59:00", 0), |
| new InstanceInfo("monthly1", "2008-05-01T00:00:00", "2008-06-01T00:00:00", 2), |
| |
| new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-05-31T00:00:00", 0), |
| new InstanceInfo("monthly2", "2008-05-01T00:10:00", "2008-05-31T23:00:00", 1), |
| new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-07-01T00:00:00", 1), |
| new InstanceInfo("monthly2", "2008-05-01T00:00:00", "2008-08-01T00:00:00", 2), |
| |
| new InstanceInfo("yearly0", "2008-05-01", "2009-04-30", 1), |
| new InstanceInfo("yearly0", "2008-05-01", "2009-05-02", 2), |
| }; |
| |
| /** |
| * This sequence of commands inserts and deletes some events. |
| */ |
| private Command[] mNormalInsertDelete = { |
| new Insert("normal0"), |
| new Insert("normal1"), |
| new Insert("normal2"), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 3), |
| new Delete("normal1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(2), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-31T00:01:00", 2), |
| new Delete("normal1", 0, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new Delete("normal2", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(1), |
| new Delete("normal0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(0), |
| }; |
| |
| /** |
| * This sequence of commands inserts and deletes some all-day events. |
| */ |
| private Command[] mAlldayInsertDelete = { |
| new Insert("allday0"), |
| new Insert("allday1"), |
| new QueryNumEvents(2), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-01T00:01:00", 0), |
| new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 2), |
| new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), |
| new Delete("allday0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(1), |
| new QueryNumInstances("2008-05-02T00:00:00", "2008-05-02T00:01:00", 1), |
| new QueryNumInstances("2008-05-03T00:00:00", "2008-05-03T00:01:00", 1), |
| new Delete("allday1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(0), |
| }; |
| |
| /** |
| * This sequence of commands inserts and deletes some repeating events. |
| */ |
| private Command[] mRecurringInsertDelete = { |
| new Insert("daily0"), |
| new Insert("daily1"), |
| new QueryNumEvents(2), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 3), |
| new QueryNumInstances("2008-05-01T01:01:00", "2008-05-02T00:01:00", 2), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 6), |
| new Delete("daily1", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(1), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-02T00:01:00", 2), |
| new QueryNumInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", 4), |
| new Delete("daily0", 1, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| new QueryNumEvents(0), |
| }; |
| |
| /** |
| * This sequence of commands creates a recurring event with a recurrence |
| * exception that moves an event outside the expansion window. It checks that the |
| * recurrence exception does not occur in the Instances database table. |
| * Bug 1642665 |
| */ |
| private Command[] mExceptionWithMovedRecurrence = { |
| new Insert("daily0"), |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", |
| "2008-05-03T00:00:00", }), |
| new Insert("except2"), |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-03T00:00:00"}), |
| }; |
| |
| /** |
| * This sequence of commands deletes (cancels) one instance of a recurrence. |
| */ |
| private Command[] mCancelInstance = { |
| new Insert("weekly0"), |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", |
| new String[] {"2008-05-06T13:00:00", "2008-05-13T13:00:00", |
| "2008-05-20T13:00:00", }), |
| new Insert("cancel0"), |
| new Update("cancel0", new KeyValue[] { |
| new KeyValue(CalendarContract.Events.STATUS, |
| Integer.toString(CalendarContract.Events.STATUS_CANCELED)), |
| }), |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00", |
| new String[] {"2008-05-06T13:00:00", |
| "2008-05-20T13:00:00", }), |
| }; |
| /** |
| * This sequence of commands creates a recurring event with a recurrence |
| * exception that moves an event from outside the expansion window into the |
| * expansion window. |
| */ |
| private Command[] mExceptionWithMovedRecurrence2 = { |
| new Insert("weekly0"), |
| new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", |
| new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", |
| "2008-12-16T13:00:00", }), |
| new Insert("except3"), |
| new VerifyAllInstances("2008-12-01T00:00:00", "2008-12-22T00:01:00", |
| new String[] {"2008-12-02T13:00:00", "2008-12-09T13:00:00", |
| "2008-12-11T13:00:00", "2008-12-16T13:00:00", }), |
| }; |
| /** |
| * This sequence of commands creates a recurring event with a recurrence |
| * exception and then changes the end time of the recurring event. It then |
| * checks that the recurrence exception does not occur in the Instances |
| * database table. |
| */ |
| private Command[] |
| mExceptionWithTruncatedRecurrence = { |
| new Insert("daily0"), |
| // Verify 4 occurrences of the "daily0" repeating event |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", |
| "2008-05-03T00:00:00", "2008-05-04T00:00:00"}), |
| new Insert("except1"), |
| new QueryNumEvents(2), |
| |
| // Verify that one of the 4 occurrences has its start time changed |
| // so that it now matches the recurrence exception. |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", |
| "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), |
| |
| // Change the end time of "daily0" but it still includes the |
| // recurrence exception. |
| new Update("daily0", new KeyValue[] { |
| new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080505T150000Z;WKST=SU"), |
| }), |
| |
| // Verify that the recurrence exception is still there |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00", |
| "2008-05-03T02:00:00", "2008-05-04T00:00:00"}), |
| // This time change the end time of "daily0" so that it excludes |
| // the recurrence exception. |
| new Update("daily0", new KeyValue[] { |
| new KeyValue(Events.RRULE, "FREQ=DAILY;UNTIL=20080502T150000Z;WKST=SU"), |
| }), |
| // The server will cancel the out-of-range exception. |
| // It would be nice for the provider to handle this automatically, |
| // but for now simulate the server-side cancel. |
| new Update("except1", new KeyValue[] { |
| new KeyValue(CalendarContract.Events.STATUS, |
| Integer.toString(CalendarContract.Events.STATUS_CANCELED)), |
| }), |
| // Verify that the recurrence exception does not appear. |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-04T00:01:00", |
| new String[] {"2008-05-01T00:00:00", "2008-05-02T00:00:00"}), |
| }; |
| |
| /** |
| * Bug 135848. Ensure that a recurrence exception is displayed even if the recurrence |
| * is not present. |
| */ |
| private Command[] mExceptionWithNoRecurrence = { |
| new Insert("except0"), |
| new QueryNumEvents(1), |
| new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-03T00:01:00", |
| new String[] {"2008-05-01T02:00:00"}), |
| }; |
| |
| private EventInfo findEvent(String name) { |
| int len = mEvents.length; |
| for (int ii = 0; ii < len; ii++) { |
| EventInfo event = mEvents[ii]; |
| if (name.equals(event.mTitle)) { |
| return event; |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| // This code here is the code that was originally in ProviderTestCase2 |
| mResolver = new MockContentResolver(); |
| |
| final String filenamePrefix = "test."; |
| RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext( |
| new MockContext2(), // The context that most methods are delegated to |
| getContext(), // The context that file methods are delegated to |
| filenamePrefix); |
| mContext = new IsolatedContext(mResolver, targetContextWrapper) { |
| @Override |
| public Object getSystemService(String name) { |
| // for accessing wakelock. |
| if (Context.POWER_SERVICE.equals(name)) { |
| return getContext().getSystemService(name); |
| } |
| return super.getSystemService(name); |
| } |
| }; |
| |
| mWorkContext = new IsolatedContext(mResolver, targetContextWrapper) { |
| @Override |
| public int getUserId() { |
| return WORK_PROFILE_USER_ID; |
| } |
| }; |
| |
| mProvider = new CalendarProvider2ForTesting() { |
| @Override |
| protected int getWorkProfileUserId() { |
| return WORK_PROFILE_USER_ID; |
| } |
| |
| @Override |
| protected int getParentUserId() { |
| return UserHandle.USER_NULL; |
| } |
| |
| @Override |
| protected void initCrossProfileCalendarHelper() { |
| mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); |
| } |
| }; |
| ProviderInfo info = new ProviderInfo(); |
| info.authority = CalendarContract.AUTHORITY; |
| mProvider.attachInfoForTesting(mContext, info); |
| |
| mWorkProfileProvider = new CalendarProvider2ForTesting() { |
| @Override |
| protected int getWorkProfileUserId() { |
| return UserHandle.USER_NULL; |
| } |
| |
| @Override |
| protected int getParentUserId() { |
| return UserHandle.myUserId(); |
| } |
| |
| @Override |
| protected void initCrossProfileCalendarHelper() { |
| mCrossProfileCalendarHelper = new MockCrossProfileCalendarHelper(mContext); |
| } |
| }; |
| ProviderInfo workProviderInfo = new ProviderInfo(); |
| workProviderInfo.authority = WORK_PROFILE_AUTHORITY; |
| mWorkProfileProvider.attachInfoForTesting(mWorkContext, info); |
| |
| mResolver.addProvider(CalendarContract.AUTHORITY, mProvider); |
| mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds")); |
| mResolver.addProvider("sync", new MockProvider("sync")); |
| mResolver.addProvider(WORK_PROFILE_AUTHORITY, mWorkProfileProvider); |
| |
| mMetaData = getProvider().mMetaData; |
| mForceDtend = false; |
| |
| CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); |
| mDb = helper.getWritableDatabase(); |
| wipeAndInitData(helper, mDb); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| try { |
| mDb.close(); |
| mDb = null; |
| getProvider().getDatabaseHelper().close(); |
| } catch (IllegalStateException e) { |
| e.printStackTrace(); |
| } |
| super.tearDown(); |
| } |
| |
| public void wipeAndInitData(SQLiteOpenHelper helper, SQLiteDatabase db) |
| throws CalendarCache.CacheException { |
| db.beginTransaction(); |
| |
| // Clean tables |
| db.delete("Calendars", null, null); |
| db.delete("Events", null, null); |
| db.delete("EventsRawTimes", null, null); |
| db.delete("Instances", null, null); |
| db.delete("CalendarMetaData", null, null); |
| db.delete("CalendarCache", null, null); |
| db.delete("Attendees", null, null); |
| db.delete("Reminders", null, null); |
| db.delete("CalendarAlerts", null, null); |
| db.delete("ExtendedProperties", null, null); |
| |
| // Set CalendarCache data |
| initCalendarCacheLocked(helper, db); |
| |
| // set CalendarMetaData data |
| long now = System.currentTimeMillis(); |
| ContentValues values = new ContentValues(); |
| values.put("localTimezone", "America/Los_Angeles"); |
| values.put("minInstance", 1207008000000L); // 1st April 2008 |
| values.put("maxInstance", now + ONE_WEEK_MILLIS); |
| db.insert("CalendarMetaData", null, values); |
| |
| db.setTransactionSuccessful(); |
| db.endTransaction(); |
| } |
| |
| private void initCalendarCacheLocked(SQLiteOpenHelper helper, SQLiteDatabase db) |
| throws CalendarCache.CacheException { |
| CalendarCache cache = new CalendarCache(helper); |
| |
| String localTimezone = TimeZone.getDefault().getID(); |
| |
| // Set initial values |
| cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_DATABASE_VERSION, "2010k"); |
| cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_TYPE, CalendarCache.TIMEZONE_TYPE_AUTO); |
| cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES, localTimezone); |
| cache.writeDataLocked(db, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS, localTimezone); |
| } |
| |
| protected CalendarProvider2ForTesting getProvider() { |
| return mProvider; |
| } |
| |
| /** |
| * Dumps the contents of the given cursor to the log. For debugging. |
| * @param cursor the database cursor |
| */ |
| private void dumpCursor(Cursor cursor) { |
| cursor.moveToPosition(-1); |
| String[] cols = cursor.getColumnNames(); |
| |
| Log.i(TAG, "dumpCursor() count: " + cursor.getCount()); |
| int index = 0; |
| while (cursor.moveToNext()) { |
| Log.i(TAG, index + " {"); |
| for (int i = 0; i < cols.length; i++) { |
| Log.i(TAG, " " + cols[i] + '=' + cursor.getString(i)); |
| } |
| Log.i(TAG, "}"); |
| index += 1; |
| } |
| cursor.moveToPosition(-1); |
| } |
| |
| private int insertCal(String name, String timezone) { |
| return insertCal(name, timezone, DEFAULT_ACCOUNT); |
| } |
| |
| /** |
| * Creates a new calendar, with the provided name, time zone, and account name. |
| * |
| * @return the new calendar's _ID value |
| */ |
| private int insertCal(String name, String timezone, String account) { |
| ContentValues m = new ContentValues(); |
| m.put(Calendars.NAME, name); |
| m.put(Calendars.CALENDAR_DISPLAY_NAME, name); |
| m.put(Calendars.CALENDAR_COLOR, 0xff123456); |
| m.put(Calendars.CALENDAR_TIME_ZONE, timezone); |
| m.put(Calendars.VISIBLE, 1); |
| m.put(Calendars.CAL_SYNC1, CALENDAR_URL); |
| m.put(Calendars.OWNER_ACCOUNT, account); |
| m.put(Calendars.ACCOUNT_NAME, account); |
| m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); |
| m.put(Calendars.SYNC_EVENTS, 1); |
| |
| Uri url = mResolver.insert( |
| addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m); |
| String id = url.getLastPathSegment(); |
| return Integer.parseInt(id); |
| } |
| |
| /** |
| * Creates a new calendar, with the provided name, time zone, and account name, but an empty |
| * owner account, which makes this calendar non-primary calendar. |
| * |
| * @return the new calendar's _ID value |
| */ |
| private int insertNonPrimaryCal(String name, String timezone, String account) { |
| ContentValues m = new ContentValues(); |
| m.put(Calendars.NAME, name); |
| m.put(Calendars.CALENDAR_DISPLAY_NAME, name); |
| m.put(Calendars.CALENDAR_COLOR, 0xff123456); |
| m.put(Calendars.CALENDAR_TIME_ZONE, timezone); |
| m.put(Calendars.VISIBLE, 1); |
| m.put(Calendars.CAL_SYNC1, CALENDAR_URL); |
| m.put(Calendars.OWNER_ACCOUNT, ""); |
| m.put(Calendars.ACCOUNT_NAME, account); |
| m.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); |
| m.put(Calendars.SYNC_EVENTS, 1); |
| |
| Uri url = mResolver.insert( |
| addSyncQueryParams(mCalendarsUri, account, DEFAULT_ACCOUNT_TYPE), m); |
| String id = url.getLastPathSegment(); |
| return Integer.parseInt(id); |
| } |
| |
| private String obsToString(Object... objs) { |
| StringBuilder bob = new StringBuilder(); |
| |
| for (Object obj : objs) { |
| bob.append(obj.toString()); |
| bob.append('#'); |
| } |
| |
| return bob.toString(); |
| } |
| |
| private Uri insertColor(long colorType, String colorKey, long color) { |
| ContentValues m = new ContentValues(); |
| m.put(Colors.ACCOUNT_NAME, DEFAULT_ACCOUNT); |
| m.put(Colors.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); |
| m.put(Colors.DATA, obsToString(colorType, colorKey, color)); |
| m.put(Colors.COLOR_TYPE, colorType); |
| m.put(Colors.COLOR_KEY, colorKey); |
| m.put(Colors.COLOR, color); |
| |
| Uri uri = CalendarContract.Colors.CONTENT_URI; |
| |
| return mResolver.insert(addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), m); |
| } |
| |
| private void updateAndCheckColor(long colorId, long colorType, String colorKey, long color) { |
| |
| Uri uri = CalendarContract.Colors.CONTENT_URI; |
| |
| final String where = Colors.ACCOUNT_NAME + "=? AND " + Colors.ACCOUNT_TYPE + "=? AND " |
| + Colors.COLOR_TYPE + "=? AND " + Colors.COLOR_KEY + "=?"; |
| |
| String[] selectionArgs = new String[] { |
| DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, Long.toString(colorType), colorKey |
| }; |
| |
| ContentValues cv = new ContentValues(); |
| cv.put(Colors.COLOR, color); |
| cv.put(Colors.DATA, obsToString(colorType, colorKey, color)); |
| |
| int count = mResolver.update( |
| addSyncQueryParams(uri, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), cv, where, |
| selectionArgs); |
| |
| checkColor(colorId, colorType, colorKey, color); |
| |
| assertEquals(1, count); |
| } |
| |
| /** |
| * Constructs a URI from a base URI (e.g. "content://com.android.calendar/calendars"), |
| * an account name, and an account type. |
| */ |
| private Uri addSyncQueryParams(Uri uri, String account, String accountType) { |
| return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") |
| .appendQueryParameter(Calendars.ACCOUNT_NAME, account) |
| .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); |
| } |
| |
| private int deleteMatchingCalendars(String selection, String[] selectionArgs) { |
| return mResolver.delete(mCalendarsUri, selection, selectionArgs); |
| } |
| |
| private Uri insertEvent(int calId, EventInfo event) { |
| return insertEvent(calId, event, null); |
| } |
| |
| private Uri insertEvent(int calId, EventInfo event, ContentValues cv) { |
| if (mWipe) { |
| // Wipe instance table so it will be regenerated |
| mMetaData.clearInstanceRange(); |
| } |
| |
| if (cv == null) { |
| cv = eventInfoToContentValues(calId, event); |
| } |
| |
| Uri url = mResolver.insert(mEventsUri, cv); |
| |
| // Create a fake _sync_id and add it to the event. Update the database |
| // directly so that we don't trigger any validation checks in the |
| // CalendarProvider. |
| long id = ContentUris.parseId(url); |
| mDb.execSQL("UPDATE Events SET _sync_id=" + mGlobalSyncId + " WHERE _id=" + id); |
| event.mSyncId = mGlobalSyncId; |
| mGlobalSyncId += 1; |
| |
| return url; |
| } |
| |
| private ContentValues eventInfoToContentValues(int calId, EventInfo event) { |
| ContentValues m = new ContentValues(); |
| m.put(Events.CALENDAR_ID, calId); |
| m.put(Events.TITLE, event.mTitle); |
| m.put(Events.DTSTART, event.mDtstart); |
| m.put(Events.ALL_DAY, event.mAllDay ? 1 : 0); |
| |
| if (event.mRrule == null || mForceDtend) { |
| // This is a normal event |
| m.put(Events.DTEND, event.mDtend); |
| m.remove(Events.DURATION); |
| } |
| if (event.mRrule != null) { |
| // This is a repeating event |
| m.put(Events.RRULE, event.mRrule); |
| m.put(Events.DURATION, event.mDuration); |
| m.remove(Events.DTEND); |
| } |
| |
| if (event.mDescription != null) { |
| m.put(Events.DESCRIPTION, event.mDescription); |
| } |
| if (event.mTimezone != null) { |
| m.put(Events.EVENT_TIMEZONE, event.mTimezone); |
| } |
| if (event.mCustomAppPackage != null) { |
| m.put(Events.CUSTOM_APP_PACKAGE, event.mCustomAppPackage); |
| } |
| if (event.mCustomAppUri != null) { |
| m.put(Events.CUSTOM_APP_URI, event.mCustomAppUri); |
| } |
| if (event.mUid2445 != null) { |
| m.put(Events.UID_2445, event.mUid2445); |
| } |
| |
| if (event.mOriginalTitle != null) { |
| // This is a recurrence exception. |
| EventInfo recur = findEvent(event.mOriginalTitle); |
| assertNotNull(recur); |
| String syncId = String.format("%d", recur.mSyncId); |
| m.put(Events.ORIGINAL_SYNC_ID, syncId); |
| m.put(Events.ORIGINAL_ALL_DAY, recur.mAllDay ? 1 : 0); |
| m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance); |
| } |
| return m; |
| } |
| |
| /** |
| * Deletes all the events that match the given title. |
| * @param title the given title to match events on |
| * @return the number of rows deleted |
| */ |
| private int deleteMatchingEvents(String title, String account, String accountType) { |
| Cursor cursor = mResolver.query(mEventsUri, new String[] { Events._ID }, |
| "title=?", new String[] { title }, null); |
| int numRows = 0; |
| while (cursor.moveToNext()) { |
| long id = cursor.getLong(0); |
| // Do delete as a sync adapter so event is really deleted, not just marked |
| // as deleted. |
| Uri uri = updatedUri(ContentUris.withAppendedId(Events.CONTENT_URI, id), true, account, |
| accountType); |
| numRows += mResolver.delete(uri, null, null); |
| } |
| cursor.close(); |
| return numRows; |
| } |
| |
| /** |
| * Updates all the events that match the given title. |
| * @param title the given title to match events on |
| * @return the number of rows updated |
| */ |
| private int updateMatchingEvents(String title, ContentValues values) { |
| String[] projection = new String[] { |
| Events._ID, |
| Events.DTSTART, |
| Events.DTEND, |
| Events.DURATION, |
| Events.ALL_DAY, |
| Events.RRULE, |
| Events.EVENT_TIMEZONE, |
| Events.ORIGINAL_SYNC_ID, |
| }; |
| Cursor cursor = mResolver.query(mEventsUri, projection, |
| "title=?", new String[] { title }, null); |
| int numRows = 0; |
| while (cursor.moveToNext()) { |
| long id = cursor.getLong(0); |
| |
| // If any of the following fields are being changed, then we need |
| // to include all of them. |
| if (values.containsKey(Events.DTSTART) || values.containsKey(Events.DTEND) |
| || values.containsKey(Events.DURATION) || values.containsKey(Events.ALL_DAY) |
| || values.containsKey(Events.RRULE) |
| || values.containsKey(Events.EVENT_TIMEZONE) |
| || values.containsKey(CalendarContract.Events.STATUS)) { |
| long dtstart = cursor.getLong(1); |
| long dtend = cursor.getLong(2); |
| String duration = cursor.getString(3); |
| boolean allDay = cursor.getInt(4) != 0; |
| String rrule = cursor.getString(5); |
| String timezone = cursor.getString(6); |
| String originalEvent = cursor.getString(7); |
| |
| if (!values.containsKey(Events.DTSTART)) { |
| values.put(Events.DTSTART, dtstart); |
| } |
| // Don't add DTEND for repeating events |
| if (!values.containsKey(Events.DTEND) && rrule == null) { |
| values.put(Events.DTEND, dtend); |
| } |
| if (!values.containsKey(Events.DURATION) && duration != null) { |
| values.put(Events.DURATION, duration); |
| } |
| if (!values.containsKey(Events.ALL_DAY)) { |
| values.put(Events.ALL_DAY, allDay ? 1 : 0); |
| } |
| if (!values.containsKey(Events.RRULE) && rrule != null) { |
| values.put(Events.RRULE, rrule); |
| } |
| if (!values.containsKey(Events.EVENT_TIMEZONE) && timezone != null) { |
| values.put(Events.EVENT_TIMEZONE, timezone); |
| } |
| if (!values.containsKey(Events.ORIGINAL_SYNC_ID) && originalEvent != null) { |
| values.put(Events.ORIGINAL_SYNC_ID, originalEvent); |
| } |
| } |
| |
| Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); |
| numRows += mResolver.update(uri, values, null, null); |
| } |
| cursor.close(); |
| return numRows; |
| } |
| |
| /** |
| * Updates the status of all the events that match the given title. |
| * @param title the given title to match events on |
| * @return the number of rows updated |
| */ |
| private int updateMatchingEventsStatusOnly(String title, ContentValues values) { |
| String[] projection = new String[] { |
| Events._ID, |
| }; |
| if (values.size() != 1 && !values.containsKey(Events.STATUS)) { |
| return 0; |
| } |
| Cursor cursor = mResolver.query(mEventsUri, projection, |
| "title=?", new String[] { title }, null); |
| int numRows = 0; |
| while (cursor.moveToNext()) { |
| long id = cursor.getLong(0); |
| |
| Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id); |
| numRows += mResolver.update(uri, values, null, null); |
| } |
| cursor.close(); |
| return numRows; |
| } |
| |
| |
| private void deleteAllEvents() { |
| mDb.execSQL("DELETE FROM Events;"); |
| mMetaData.clearInstanceRange(); |
| } |
| |
| /** |
| * Creates an updated URI that includes query parameters that identify the source as a |
| * sync adapter. |
| */ |
| static Uri asSyncAdapter(Uri uri, String account, String accountType) { |
| return uri.buildUpon() |
| .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER, |
| "true") |
| .appendQueryParameter(Calendars.ACCOUNT_NAME, account) |
| .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); |
| } |
| |
| public void testInsertUpdateDeleteColor() throws Exception { |
| // Calendar Color |
| long colorType = Colors.TYPE_CALENDAR; |
| String colorKey = "123"; |
| long colorValue = 11; |
| long colorId = insertAndCheckColor(colorType, colorKey, colorValue); |
| |
| try { |
| insertAndCheckColor(colorType, colorKey, colorValue); |
| fail("Expected to fail with duplicate insertion"); |
| } catch (IllegalArgumentException iae) { |
| // good |
| } |
| |
| // Test Update |
| colorValue += 11; |
| updateAndCheckColor(colorId, colorType, colorKey, colorValue); |
| |
| // Event Color |
| colorType = Colors.TYPE_EVENT; |
| colorValue += 11; |
| colorId = insertAndCheckColor(colorType, colorKey, colorValue); |
| try { |
| insertAndCheckColor(colorType, colorKey, colorValue); |
| fail("Expected to fail with duplicate insertion"); |
| } catch (IllegalArgumentException iae) { |
| // good |
| } |
| |
| // Create an event with the old color value. |
| int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| String title = "colorTest"; |
| ContentValues cv = this.eventInfoToContentValues(calendarId0, mEvents[0]); |
| cv.put(Events.EVENT_COLOR_KEY, colorKey); |
| cv.put(Events.TITLE, title); |
| Uri uri = insertEvent(calendarId0, mEvents[0], cv); |
| Cursor c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null); |
| try { |
| // Confirm the color is set. |
| c.moveToFirst(); |
| assertEquals(colorValue, c.getInt(0)); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // Test Update |
| colorValue += 11; |
| updateAndCheckColor(colorId, colorType, colorKey, colorValue); |
| |
| // Check if color was updated in event. |
| c = mResolver.query(uri, new String[] {Events.EVENT_COLOR}, null, null, null); |
| try { |
| c.moveToFirst(); |
| assertEquals(colorValue, c.getInt(0)); |
| } finally { |
| if (c != null) { |
| c.close(); |
| } |
| } |
| |
| // Test Delete |
| Uri colSyncUri = asSyncAdapter(Colors.CONTENT_URI, DEFAULT_ACCOUNT, |
| DEFAULT_ACCOUNT_TYPE); |
| try { |
| // Delete should fail if color referenced by an event. |
| mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX, |
| new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey}); |
| fail("Should not allow deleting referenced color"); |
| } catch (UnsupportedOperationException e) { |
| // Exception expected. |
| } |
| Cursor cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY}, |
| Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?", |
| new String[] {colorKey, Long.toString(colorType)}, null); |
| assertEquals(1, cursor.getCount()); |
| |
| // Try again, by deleting the event, then the color. |
| assertEquals(1, deleteMatchingEvents(title, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE)); |
| mResolver.delete(colSyncUri, WHERE_COLOR_ACCOUNT_AND_INDEX, |
| new String[] {DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE, colorKey}); |
| cursor = mResolver.query(Colors.CONTENT_URI, new String[] {Colors.COLOR_KEY}, |
| Colors.COLOR_KEY + "=? AND " + Colors.COLOR_TYPE + "=?", |
| new String[] {colorKey, Long.toString(colorType)}, null); |
| assertEquals(0, cursor.getCount()); |
| } |
| |
| private void checkColor(long colorId, long colorType, String colorKey, long color) { |
| String[] projection = new String[] { |
| Colors.ACCOUNT_NAME, // 0 |
| Colors.ACCOUNT_TYPE, // 1 |
| Colors.COLOR_TYPE, // 2 |
| Colors.COLOR_KEY, // 3 |
| Colors.COLOR, // 4 |
| Colors._ID, // 5 |
| Colors.DATA, // 6 |
| }; |
| Cursor cursor = mResolver.query(Colors.CONTENT_URI, projection, Colors.COLOR_KEY |
| + "=? AND " + Colors.COLOR_TYPE + "=?", new String[] { |
| colorKey, Long.toString(colorType) |
| }, null /* sortOrder */); |
| |
| assertEquals(1, cursor.getCount()); |
| |
| assertTrue(cursor.moveToFirst()); |
| assertEquals(DEFAULT_ACCOUNT, cursor.getString(0)); |
| assertEquals(DEFAULT_ACCOUNT_TYPE, cursor.getString(1)); |
| assertEquals(colorType, cursor.getLong(2)); |
| assertEquals(colorKey, cursor.getString(3)); |
| assertEquals(color, cursor.getLong(4)); |
| assertEquals(colorId, cursor.getLong(5)); |
| assertEquals(obsToString(colorType, colorKey, color), cursor.getString(6)); |
| cursor.close(); |
| } |
| |
| private long insertAndCheckColor(long colorType, String colorKey, long color) { |
| Uri uri = insertColor(colorType, colorKey, color); |
| long id = Long.parseLong(uri.getLastPathSegment()); |
| |
| checkColor(id, colorType, colorKey, color); |
| return id; |
| } |
| |
| public void testInsertNormalEvents() throws Exception { |
| final int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| Cursor cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(0, cursor.getCount()); |
| cursor.close(); |
| |
| // Keep track of the number of normal events |
| int numOfInserts = 0; |
| |
| // "begin" is the earliest start time of all the normal events, |
| // and "end" is the latest end time of all the normal events. |
| long begin = 0, end = 0; |
| |
| int len = mEvents.length; |
| Uri[] uris = new Uri[len]; |
| ContentValues[] cvs = new ContentValues[len]; |
| for (int ii = 0; ii < len; ii++) { |
| EventInfo event = mEvents[ii]; |
| // Skip repeating events and recurrence exceptions |
| if (event.mRrule != null || event.mOriginalTitle != null) { |
| continue; |
| } |
| if (numOfInserts == 0) { |
| begin = event.mDtstart; |
| end = event.mDtend; |
| } else { |
| if (begin > event.mDtstart) { |
| begin = event.mDtstart; |
| } |
| if (end < event.mDtend) { |
| end = event.mDtend; |
| } |
| } |
| |
| cvs[ii] = eventInfoToContentValues(calId, event); |
| uris[ii] = insertEvent(calId, event, cvs[ii]); |
| numOfInserts += 1; |
| } |
| |
| // Verify |
| for (int i = 0; i < len; i++) { |
| if (cvs[i] == null) continue; |
| assertNotNull(uris[i]); |
| cursor = mResolver.query(uris[i], null, null, null, null); |
| assertEquals("Item " + i + " not found", 1, cursor.getCount()); |
| verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor); |
| cursor.close(); |
| } |
| |
| // query all |
| cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(numOfInserts, cursor.getCount()); |
| cursor.close(); |
| |
| // Check that the Instances table has one instance of each of the |
| // normal events. |
| cursor = queryInstances(begin, end); |
| assertEquals(numOfInserts, cursor.getCount()); |
| cursor.close(); |
| } |
| |
| public void testInsertRepeatingEvents() throws Exception { |
| Cursor cursor; |
| Uri url = null; |
| |
| int calId = insertCal("Calendar0", "America/Los_Angeles"); |
| |
| cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(0, cursor.getCount()); |
| cursor.close(); |
| |
| // Keep track of the number of repeating events |
| int numOfInserts = 0; |
| |
| int len = mEvents.length; |
| Uri[] uris = new Uri[len]; |
| ContentValues[] cvs = new ContentValues[len]; |
| for (int ii = 0; ii < len; ii++) { |
| EventInfo event = mEvents[ii]; |
| // Skip normal events |
| if (event.mRrule == null) { |
| continue; |
| } |
| cvs[ii] = eventInfoToContentValues(calId, event); |
| uris[ii] = insertEvent(calId, event, cvs[ii]); |
| numOfInserts += 1; |
| } |
| |
| // Verify |
| for (int i = 0; i < len; i++) { |
| if (cvs[i] == null) continue; |
| assertNotNull(uris[i]); |
| cursor = mResolver.query(uris[i], null, null, null, null); |
| assertEquals("Item " + i + " not found", 1, cursor.getCount()); |
| verifyContentValueAgainstCursor(cvs[i], cvs[i].keySet(), cursor); |
| cursor.close(); |
| } |
| |
| // query all |
| cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(numOfInserts, cursor.getCount()); |
| cursor.close(); |
| } |
| |
| // Force a dtend value to be set and make sure instance expansion still works |
| public void testInstanceRangeDtend() throws Exception { |
| mForceDtend = true; |
| testInstanceRange(); |
| } |
| |
| public void testInstanceRange() throws Exception { |
| Cursor cursor; |
| Uri url = null; |
| |
| int calId = insertCal("Calendar0", "America/Los_Angeles"); |
| |
| cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(0, cursor.getCount()); |
| cursor.close(); |
| |
| int len = mInstanceRanges.length; |
| for (int ii = 0; ii < len; ii++) { |
| InstanceInfo instance = mInstanceRanges[ii]; |
| EventInfo event = instance.mEvent; |
| url = insertEvent(calId, event); |
| cursor = queryInstances(instance.mBegin, instance.mEnd); |
| if (instance.mExpectedOccurrences != cursor.getCount()) { |
| Log.e(TAG, "Test failed! Instance index: " + ii); |
| Log.e(TAG, "title: " + event.mTitle + " desc: " + event.mDescription |
| + " [begin,end]: [" + instance.mBegin + " " + instance.mEnd + "]" |
| + " expected: " + instance.mExpectedOccurrences); |
| dumpCursor(cursor); |
| } |
| assertEquals(instance.mExpectedOccurrences, cursor.getCount()); |
| cursor.close(); |
| // Delete as sync_adapter so event is really deleted. |
| int rows = mResolver.delete( |
| updatedUri(url, true, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| null /* selection */, null /* selection args */); |
| assertEquals(1, rows); |
| } |
| } |
| |
| public static <T> void assertArrayEquals(T[] expected, T[] actual) { |
| if (!Arrays.equals(expected, actual)) { |
| fail("expected:<" + Arrays.toString(expected) + |
| "> but was:<" + Arrays.toString(actual) + ">"); |
| } |
| } |
| |
| @SmallTest @Smoke |
| public void testEscapeSearchToken() { |
| String token = "test"; |
| String expected = "test"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "%"; |
| expected = "#%"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "_"; |
| expected = "#_"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "#"; |
| expected = "##"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "##"; |
| expected = "####"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "%_#"; |
| expected = "#%#_##"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| |
| token = "blah%blah"; |
| expected = "blah#%blah"; |
| assertEquals(expected, mProvider.escapeSearchToken(token)); |
| } |
| |
| @SmallTest @Smoke |
| public void testTokenizeSearchQuery() { |
| String query = ""; |
| String[] expectedTokens = new String[] {}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "a"; |
| expectedTokens = new String[] {"a"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "word"; |
| expectedTokens = new String[] {"word"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "two words"; |
| expectedTokens = new String[] {"two", "words"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "test, punctuation."; |
| expectedTokens = new String[] {"test", "punctuation"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "\"test phrase\""; |
| expectedTokens = new String[] {"test phrase"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "unquoted \"this is quoted\""; |
| expectedTokens = new String[] {"unquoted", "this is quoted"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = " \"this is quoted\" unquoted "; |
| expectedTokens = new String[] {"this is quoted", "unquoted"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "escap%e m_e"; |
| expectedTokens = new String[] {"escap#%e", "m#_e"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "'a bunch' of malformed\" things"; |
| expectedTokens = new String[] {"a", "bunch", "of", "malformed", "things"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| |
| query = "''''''....,.''trim punctuation"; |
| expectedTokens = new String[] {"trim", "punctuation"}; |
| assertArrayEquals(expectedTokens, mProvider.tokenizeSearchQuery(query)); |
| } |
| |
| @SmallTest @Smoke |
| public void testConstructSearchWhere() { |
| String[] tokens = new String[] {"red"}; |
| String expected = "(title LIKE ? ESCAPE \"#\" OR " |
| + "description LIKE ? ESCAPE \"#\" OR " |
| + "eventLocation LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; |
| assertEquals(expected, mProvider.constructSearchWhere(tokens)); |
| |
| tokens = new String[] {}; |
| expected = ""; |
| assertEquals(expected, mProvider.constructSearchWhere(tokens)); |
| |
| tokens = new String[] {"red", "green"}; |
| expected = "(title LIKE ? ESCAPE \"#\" OR " |
| + "description LIKE ? ESCAPE \"#\" OR " |
| + "eventLocation LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND " |
| + "(title LIKE ? ESCAPE \"#\" OR " |
| + "description LIKE ? ESCAPE \"#\" OR " |
| + "eventLocation LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; |
| assertEquals(expected, mProvider.constructSearchWhere(tokens)); |
| |
| tokens = new String[] {"red blue", "green"}; |
| expected = "(title LIKE ? ESCAPE \"#\" OR " |
| + "description LIKE ? ESCAPE \"#\" OR " |
| + "eventLocation LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" ) AND " |
| + "(title LIKE ? ESCAPE \"#\" OR " |
| + "description LIKE ? ESCAPE \"#\" OR " |
| + "eventLocation LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeEmail) LIKE ? ESCAPE \"#\" OR " |
| + "group_concat(attendeeName) LIKE ? ESCAPE \"#\" )"; |
| assertEquals(expected, mProvider.constructSearchWhere(tokens)); |
| } |
| |
| @SmallTest @Smoke |
| public void testConstructSearchArgs() { |
| String[] tokens = new String[] {"red"}; |
| String[] expected = new String[] {"%red%", "%red%", |
| "%red%", "%red%", "%red%" }; |
| assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); |
| |
| tokens = new String[] {"red", "blue"}; |
| expected = new String[] { "%red%", "%red%", "%red%", |
| "%red%", "%red%", "%blue%", "%blue%", |
| "%blue%", "%blue%","%blue%"}; |
| assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); |
| |
| tokens = new String[] {}; |
| expected = new String[] {}; |
| assertArrayEquals(expected, mProvider.constructSearchArgs(tokens)); |
| } |
| |
| public void testInstanceSearchQuery() throws Exception { |
| final String[] PROJECTION = new String[] { |
| Instances.TITLE, // 0 |
| Instances.EVENT_LOCATION, // 1 |
| Instances.ALL_DAY, // 2 |
| Instances.CALENDAR_COLOR, // 3 |
| Instances.EVENT_TIMEZONE, // 4 |
| Instances.EVENT_ID, // 5 |
| Instances.BEGIN, // 6 |
| Instances.END, // 7 |
| Instances._ID, // 8 |
| Instances.START_DAY, // 9 |
| Instances.END_DAY, // 10 |
| Instances.START_MINUTE, // 11 |
| Instances.END_MINUTE, // 12 |
| Instances.HAS_ALARM, // 13 |
| Instances.RRULE, // 14 |
| Instances.RDATE, // 15 |
| Instances.SELF_ATTENDEE_STATUS, // 16 |
| Events.ORGANIZER, // 17 |
| Events.GUESTS_CAN_MODIFY, // 18 |
| }; |
| |
| String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW; |
| String where = Instances.SELF_ATTENDEE_STATUS + "!=" + |
| CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED; |
| |
| int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| final String START = "2008-05-01T00:00:00"; |
| final String END = "2008-05-01T20:00:00"; |
| |
| EventInfo event1 = new EventInfo("search orange", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE); |
| event1.mDescription = "this is description1"; |
| |
| EventInfo event2 = new EventInfo("search purple", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE); |
| event2.mDescription = "lasers, out of nowhere"; |
| |
| EventInfo event3 = new EventInfo("", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE); |
| event3.mDescription = "kapow"; |
| |
| EventInfo[] events = { event1, event2, event3 }; |
| |
| insertEvent(calId, events[0]); |
| insertEvent(calId, events[1]); |
| insertEvent(calId, events[2]); |
| |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(START); |
| long startMs = time.toMillis(true /* ignoreDst */); |
| // Query starting from way in the past to one hour into the event. |
| // Query is more than 2 months so the range won't get extended by the provider. |
| Cursor cursor = null; |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "search", where, null, orderBy); |
| assertEquals(2, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "purple", where, null, orderBy); |
| assertEquals(1, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "puurple", where, null, orderBy); |
| assertEquals(0, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "purple lasers", where, null, orderBy); |
| assertEquals(1, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "lasers kapow", where, null, orderBy); |
| assertEquals(0, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "\"search purple\"", where, null, orderBy); |
| assertEquals(1, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "\"purple search\"", where, null, orderBy); |
| assertEquals(0, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| |
| try { |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, |
| startMs + DateUtils.HOUR_IN_MILLIS, |
| "%", where, null, orderBy); |
| assertEquals(0, cursor.getCount()); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| |
| public void testDeleteCalendar() throws Exception { |
| int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); |
| insertEvent(calendarId0, mEvents[0]); |
| insertEvent(calendarId1, mEvents[1]); |
| // Should have 2 calendars and 2 events |
| testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 2); |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 2); |
| |
| int deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI, |
| "ownerAccount='user2@google.com'", null /* selectionArgs */); |
| |
| assertEquals(1, deletes); |
| // Should have 1 calendar and 1 event |
| testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 1); |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 1); |
| |
| deletes = mResolver.delete(Uri.withAppendedPath(CalendarContract.Calendars.CONTENT_URI, |
| String.valueOf(calendarId0)), |
| null /* selection*/ , null /* selectionArgs */); |
| |
| assertEquals(1, deletes); |
| // Should have 0 calendars and 0 events |
| testQueryCount(CalendarContract.Calendars.CONTENT_URI, null /* where */, 0); |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null /* where */, 0); |
| |
| deletes = mResolver.delete(CalendarContract.Calendars.CONTENT_URI, |
| "ownerAccount=?", new String[] {"user2@google.com"} /* selectionArgs */); |
| |
| assertEquals(0, deletes); |
| } |
| |
| public void testCalendarAlerts() throws Exception { |
| // This projection is from AlertActivity; want to make sure it works. |
| String[] projection = new String[] { |
| CalendarContract.CalendarAlerts._ID, // 0 |
| CalendarContract.CalendarAlerts.TITLE, // 1 |
| CalendarContract.CalendarAlerts.EVENT_LOCATION, // 2 |
| CalendarContract.CalendarAlerts.ALL_DAY, // 3 |
| CalendarContract.CalendarAlerts.BEGIN, // 4 |
| CalendarContract.CalendarAlerts.END, // 5 |
| CalendarContract.CalendarAlerts.EVENT_ID, // 6 |
| CalendarContract.CalendarAlerts.CALENDAR_COLOR, // 7 |
| CalendarContract.CalendarAlerts.RRULE, // 8 |
| CalendarContract.CalendarAlerts.HAS_ALARM, // 9 |
| CalendarContract.CalendarAlerts.STATE, // 10 |
| CalendarContract.CalendarAlerts.ALARM_TIME, // 11 |
| }; |
| |
| mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE); |
| String calendarIdString = Integer.toString(mCalendarId); |
| checkEvents(0, mDb, calendarIdString); |
| Uri eventUri = insertEvent(mCalendarId, findEvent("normal0")); |
| checkEvents(1, mDb, calendarIdString); |
| long eventId = ContentUris.parseId(eventUri); |
| |
| Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */, |
| 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */); |
| CalendarContract.CalendarAlerts.insert(mResolver, eventId /* eventId */, |
| 2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */); |
| |
| // Regular query |
| Cursor cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI, projection, |
| null /* selection */, null /* selectionArgs */, null /* sortOrder */); |
| |
| assertEquals(2, cursor.getCount()); |
| cursor.close(); |
| |
| // Instance query |
| cursor = mResolver.query(alertUri, projection, |
| null /* selection */, null /* selectionArgs */, null /* sortOrder */); |
| |
| assertEquals(1, cursor.getCount()); |
| cursor.close(); |
| |
| // Grouped by event query |
| cursor = mResolver.query(CalendarContract.CalendarAlerts.CONTENT_URI_BY_INSTANCE, |
| projection, null /* selection */, null /* selectionArgs */, null /* sortOrder */); |
| |
| assertEquals(1, cursor.getCount()); |
| cursor.close(); |
| } |
| |
| public void testInsertAlertToNonExistentEvent() { |
| Uri alertUri = CalendarContract.CalendarAlerts.insert(mResolver, 1 /* eventId */, |
| 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */); |
| assertEquals(null, alertUri); |
| } |
| |
| public void testInsertReminderToNonExistentEvent() { |
| ContentValues reminder = new ContentValues(); |
| reminder.put(CalendarContract.Reminders.MINUTES, 30); |
| reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL); |
| reminder.put(CalendarContract.Attendees.EVENT_ID, 1); |
| Uri reminderUri = mResolver.insert( |
| updatedUri(CalendarContract.Reminders.CONTENT_URI, true, DEFAULT_ACCOUNT, |
| DEFAULT_ACCOUNT_TYPE), reminder); |
| assertEquals(null, reminderUri); |
| } |
| |
| public void testInsertAttendeeToNonExistentEvent() { |
| ContentValues attendee = new ContentValues(); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, |
| CalendarContract.Attendees.TYPE_REQUIRED); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, |
| CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); |
| attendee.put(CalendarContract.Attendees.EVENT_ID, 1); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1"); |
| Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); |
| assertEquals(null, attendeesUri); |
| } |
| |
| public void testInsertExtendedPropertyToNonExistentEvent() { |
| ContentValues extended = new ContentValues(); |
| extended.put(CalendarContract.ExtendedProperties.NAME, "foo"); |
| extended.put(CalendarContract.ExtendedProperties.VALUE, "bar"); |
| extended.put(CalendarContract.ExtendedProperties.EVENT_ID, 1); |
| |
| Uri extendedUri = mResolver.insert( |
| updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, true, |
| DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); |
| assertEquals(null, extendedUri); |
| } |
| |
| void checkEvents(int count, SQLiteDatabase db) { |
| Cursor cursor = db.query("Events", null, null, null, null, null, null); |
| try { |
| assertEquals(count, cursor.getCount()); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| void checkEvents(int count, SQLiteDatabase db, String calendar) { |
| Cursor cursor = db.query("Events", null, Events.CALENDAR_ID + "=?", new String[] {calendar}, |
| null, null, null); |
| try { |
| assertEquals(count, cursor.getCount()); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| |
| // TODO Reenable this when we are ready to work on this |
| // |
| // public void testToShowInsertIsSlowForRecurringEvents() throws Exception { |
| // mCalendarId = insertCal("CalendarTestToShowInsertIsSlowForRecurringEvents", DEFAULT_TIMEZONE); |
| // String calendarIdString = Integer.toString(mCalendarId); |
| // long testStart = System.currentTimeMillis(); |
| // |
| // final int testTrials = 100; |
| // |
| // for (int i = 0; i < testTrials; i++) { |
| // checkEvents(i, mDb, calendarIdString); |
| // long insertStartTime = System.currentTimeMillis(); |
| // Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); |
| // Log.e(TAG, i + ") insertion time " + (System.currentTimeMillis() - insertStartTime)); |
| // } |
| // Log.e(TAG, " Avg insertion time = " + (System.currentTimeMillis() - testStart)/testTrials); |
| // } |
| |
| /** |
| * Test attendee processing |
| * @throws Exception |
| */ |
| public void testAttendees() throws Exception { |
| mCalendarId = insertCal("CalendarTestAttendees", DEFAULT_TIMEZONE); |
| String calendarIdString = Integer.toString(mCalendarId); |
| checkEvents(0, mDb, calendarIdString); |
| Uri eventUri = insertEvent(mCalendarId, findEvent("normal0")); |
| checkEvents(1, mDb, calendarIdString); |
| long eventId = ContentUris.parseId(eventUri); |
| |
| ContentValues attendee = new ContentValues(); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, |
| CalendarContract.Attendees.TYPE_REQUIRED); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, |
| CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); |
| attendee.put(CalendarContract.Attendees.EVENT_ID, eventId); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID1"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, "IDNS1"); |
| Uri attendeesUri = mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); |
| |
| Cursor cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null, |
| "event_id=" + eventId, null, null); |
| assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1, |
| cursor.getCount()); |
| Set<String> attendeeColumns = attendee.keySet(); |
| verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor); |
| cursor.close(); |
| |
| cursor = mResolver.query(eventUri, null, null, null, null); |
| // TODO figure out why this test fails. App works fine for this case. |
| assertEquals("Created event is missing - cannot find EventUri = " + eventUri, 1, |
| cursor.getCount()); |
| int selfColumn = cursor.getColumnIndex(CalendarContract.Events.SELF_ATTENDEE_STATUS); |
| cursor.moveToNext(); |
| long selfAttendeeStatus = cursor.getInt(selfColumn); |
| assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED, selfAttendeeStatus); |
| cursor.close(); |
| |
| // Update status to declined and change identity |
| ContentValues attendeeUpdate = new ContentValues(); |
| attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, "ID2"); |
| attendeeUpdate.put(CalendarContract.Attendees.ATTENDEE_STATUS, |
| CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS, |
| CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); |
| mResolver.update(attendeesUri, attendeeUpdate, null, null); |
| |
| // Check in attendees table |
| cursor = mResolver.query(attendeesUri, null, null, null, null); |
| cursor.moveToNext(); |
| verifyContentValueAgainstCursor(attendee, attendeeColumns, cursor); |
| cursor.close(); |
| |
| // Test that the self status in events table is updated |
| cursor = mResolver.query(eventUri, null, null, null, null); |
| cursor.moveToNext(); |
| selfAttendeeStatus = cursor.getInt(selfColumn); |
| assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); |
| cursor.close(); |
| |
| // Add another attendee |
| attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Dude"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, "dude@dude.com"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_STATUS, |
| CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED); |
| mResolver.insert(CalendarContract.Attendees.CONTENT_URI, attendee); |
| |
| cursor = mResolver.query(CalendarContract.Attendees.CONTENT_URI, null, |
| "event_id=" + eventId, null, null); |
| assertEquals(2, cursor.getCount()); |
| cursor.close(); |
| |
| cursor = mResolver.query(eventUri, null, null, null, null); |
| cursor.moveToNext(); |
| selfAttendeeStatus = cursor.getInt(selfColumn); |
| assertEquals(CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus); |
| cursor.close(); |
| } |
| |
| private void verifyContentValueAgainstCursor(ContentValues cv, |
| Set<String> keys, Cursor cursor) { |
| cursor.moveToFirst(); |
| for (String key : keys) { |
| assertEquals(cv.get(key).toString(), |
| cursor.getString(cursor.getColumnIndex(key))); |
| } |
| cursor.close(); |
| } |
| |
| /** |
| * Test the event's dirty status and clear it. |
| * |
| * @param eventId event to fetch. |
| * @param wanted the wanted dirty status |
| */ |
| private void testAndClearDirty(long eventId, int wanted) { |
| Cursor cursor = mResolver.query( |
| ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId), |
| null, null, null, null); |
| try { |
| assertEquals("Event count", 1, cursor.getCount()); |
| cursor.moveToNext(); |
| int dirty = cursor.getInt(cursor.getColumnIndex(CalendarContract.Events.DIRTY)); |
| assertEquals("dirty flag", wanted, dirty); |
| if (dirty == 1) { |
| // Have to access database directly since provider will set dirty again. |
| mDb.execSQL("UPDATE Events SET " + Events.DIRTY + "=0 WHERE _id=" + eventId); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * Test the count of results from a query. |
| * @param uri The URI to query |
| * @param where The where string or null. |
| * @param wanted The number of results wanted. An assertion is thrown if it doesn't match. |
| */ |
| private void testQueryCount(Uri uri, String where, int wanted) { |
| Cursor cursor = mResolver.query(uri, null/* projection */, where, null /* selectionArgs */, |
| null /* sortOrder */); |
| try { |
| assertEquals("query results", wanted, cursor.getCount()); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * Test dirty flag processing. |
| * @throws Exception |
| */ |
| public void testDirty() throws Exception { |
| internalTestDirty(false); |
| } |
| |
| /** |
| * Test dirty flag processing for updates from a sync adapter. |
| * @throws Exception |
| */ |
| public void testDirtyWithSyncAdapter() throws Exception { |
| internalTestDirty(true); |
| } |
| |
| /** |
| * Adds CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation. Otherwise, |
| * returns the original URI. |
| */ |
| private Uri updatedUri(Uri uri, boolean syncAdapter, String account, String accountType) { |
| if (syncAdapter) { |
| return addSyncQueryParams(uri, account, accountType); |
| } else { |
| return uri; |
| } |
| } |
| |
| /** |
| * Test dirty flag processing either for syncAdapter operations or client operations. |
| * The main difference is syncAdapter operations don't set the dirty bit. |
| */ |
| private void internalTestDirty(boolean syncAdapter) throws Exception { |
| mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| long now = System.currentTimeMillis(); |
| long begin = (now / 1000) * 1000; |
| long end = begin + ONE_HOUR_MILLIS; |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.set(begin); |
| String startDate = time.format3339(false); |
| time.set(end); |
| String endDate = time.format3339(false); |
| |
| EventInfo eventInfo = new EventInfo("current", startDate, endDate, false); |
| Uri eventUri = insertEvent(mCalendarId, eventInfo); |
| |
| long eventId = ContentUris.parseId(eventUri); |
| testAndClearDirty(eventId, 1); |
| |
| ContentValues attendee = new ContentValues(); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Joe"); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_EMAIL, DEFAULT_ACCOUNT); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_TYPE, |
| CalendarContract.Attendees.TYPE_REQUIRED); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, |
| CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); |
| attendee.put(CalendarContract.Attendees.EVENT_ID, eventId); |
| |
| Uri attendeeUri = mResolver.insert( |
| updatedUri(CalendarContract.Attendees.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT, |
| DEFAULT_ACCOUNT_TYPE), |
| attendee); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1); |
| |
| ContentValues reminder = new ContentValues(); |
| reminder.put(CalendarContract.Reminders.MINUTES, 30); |
| reminder.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_EMAIL); |
| reminder.put(CalendarContract.Attendees.EVENT_ID, eventId); |
| |
| Uri reminderUri = mResolver.insert( |
| updatedUri(CalendarContract.Reminders.CONTENT_URI, syncAdapter, DEFAULT_ACCOUNT, |
| DEFAULT_ACCOUNT_TYPE), reminder); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 1); |
| |
| long alarmTime = begin + 5 * ONE_MINUTE_MILLIS; |
| |
| ContentValues alert = new ContentValues(); |
| alert.put(CalendarContract.CalendarAlerts.BEGIN, begin); |
| alert.put(CalendarContract.CalendarAlerts.END, end); |
| alert.put(CalendarContract.CalendarAlerts.ALARM_TIME, alarmTime); |
| alert.put(CalendarContract.CalendarAlerts.CREATION_TIME, now); |
| alert.put(CalendarContract.CalendarAlerts.RECEIVED_TIME, now); |
| alert.put(CalendarContract.CalendarAlerts.NOTIFY_TIME, now); |
| alert.put(CalendarContract.CalendarAlerts.STATE, |
| CalendarContract.CalendarAlerts.STATE_SCHEDULED); |
| alert.put(CalendarContract.CalendarAlerts.MINUTES, 30); |
| alert.put(CalendarContract.CalendarAlerts.EVENT_ID, eventId); |
| |
| Uri alertUri = mResolver.insert( |
| updatedUri(CalendarContract.CalendarAlerts.CONTENT_URI, syncAdapter, |
| DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert); |
| // Alerts don't dirty the event |
| testAndClearDirty(eventId, 0); |
| testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); |
| |
| ContentValues extended = new ContentValues(); |
| extended.put(CalendarContract.ExtendedProperties.NAME, "foo"); |
| extended.put(CalendarContract.ExtendedProperties.VALUE, "bar"); |
| extended.put(CalendarContract.ExtendedProperties.EVENT_ID, eventId); |
| |
| Uri extendedUri = null; |
| if (syncAdapter) { |
| // Only the sync adapter is allowed to modify ExtendedProperties. |
| extendedUri = mResolver.insert( |
| updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter, |
| DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, |
| "event_id=" + eventId, 1); |
| } else { |
| // Confirm that inserting as app fails. |
| try { |
| extendedUri = mResolver.insert( |
| updatedUri(CalendarContract.ExtendedProperties.CONTENT_URI, syncAdapter, |
| DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), extended); |
| fail("Only sync adapter should be allowed to insert into ExtendedProperties"); |
| } catch (IllegalArgumentException iae) {} |
| } |
| |
| // Now test updates |
| |
| attendee = new ContentValues(); |
| attendee.put(CalendarContract.Attendees.ATTENDEE_NAME, "Sam"); |
| |
| assertEquals("update", 1, mResolver.update( |
| updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| attendee, |
| null /* where */, null /* selectionArgs */)); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| |
| testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 1); |
| |
| alert = new ContentValues(); |
| alert.put(CalendarContract.CalendarAlerts.STATE, |
| CalendarContract.CalendarAlerts.STATE_DISMISSED); |
| |
| assertEquals("update", 1, mResolver.update( |
| updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), alert, |
| null /* where */, null /* selectionArgs */)); |
| // Alerts don't dirty the event |
| testAndClearDirty(eventId, 0); |
| testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1); |
| |
| extended = new ContentValues(); |
| extended.put(CalendarContract.ExtendedProperties.VALUE, "baz"); |
| |
| if (syncAdapter) { |
| assertEquals("update", 1, mResolver.update( |
| updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| extended, |
| null /* where */, null /* selectionArgs */)); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, |
| "event_id=" + eventId, 1); |
| } |
| |
| // Now test deletes |
| |
| assertEquals("delete", 1, mResolver.delete( |
| updatedUri(attendeeUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| null, null /* selectionArgs */)); |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.Attendees.CONTENT_URI, "event_id=" + eventId, 0); |
| |
| assertEquals("delete", 1, mResolver.delete( |
| updatedUri(reminderUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| null /* where */, null /* selectionArgs */)); |
| |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.Reminders.CONTENT_URI, "event_id=" + eventId, 0); |
| |
| assertEquals("delete", 1, mResolver.delete( |
| updatedUri(alertUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| null /* where */, null /* selectionArgs */)); |
| |
| // Alerts don't dirty the event |
| testAndClearDirty(eventId, 0); |
| testQueryCount(CalendarContract.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0); |
| |
| if (syncAdapter) { |
| assertEquals("delete", 1, mResolver.delete( |
| updatedUri(extendedUri, syncAdapter, DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE), |
| null /* where */, null /* selectionArgs */)); |
| |
| testAndClearDirty(eventId, syncAdapter ? 0 : 1); |
| testQueryCount(CalendarContract.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, |
| 0); |
| } |
| } |
| |
| /** |
| * Test calendar deletion |
| * @throws Exception |
| */ |
| public void testCalendarDeletion() throws Exception { |
| mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| Uri eventUri = insertEvent(mCalendarId, findEvent("daily0")); |
| long eventId = ContentUris.parseId(eventUri); |
| testAndClearDirty(eventId, 1); |
| Uri eventUri1 = insertEvent(mCalendarId, findEvent("daily1")); |
| long eventId1 = ContentUris.parseId(eventUri); |
| assertEquals("delete", 1, mResolver.delete(eventUri1, null, null)); |
| // Calendar has one event and one deleted event |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); |
| |
| assertEquals("delete", 1, mResolver.delete(CalendarContract.Calendars.CONTENT_URI, |
| "_id=" + mCalendarId, null)); |
| // Calendar should be deleted |
| testQueryCount(CalendarContract.Calendars.CONTENT_URI, null, 0); |
| // Event should be gone |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null, 0); |
| } |
| |
| /** |
| * Test multiple account support. |
| */ |
| public void testMultipleAccounts() throws Exception { |
| mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| int calendarId1 = insertCal("Calendar1", DEFAULT_TIMEZONE, "user2@google.com"); |
| Uri eventUri0 = insertEvent(mCalendarId, findEvent("daily0")); |
| Uri eventUri1 = insertEvent(calendarId1, findEvent("daily1")); |
| |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); |
| Uri eventsWithAccount = CalendarContract.Events.CONTENT_URI.buildUpon() |
| .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_NAME, DEFAULT_ACCOUNT) |
| .appendQueryParameter(CalendarContract.EventsEntity.ACCOUNT_TYPE, |
| DEFAULT_ACCOUNT_TYPE) |
| .build(); |
| // Only one event for that account |
| testQueryCount(eventsWithAccount, null, 1); |
| |
| // Test deletion with account and selection |
| |
| long eventId = ContentUris.parseId(eventUri1); |
| // Wrong account, should not be deleted |
| assertEquals("delete", 0, mResolver.delete( |
| updatedUri(eventsWithAccount, true /* syncAdapter */, DEFAULT_ACCOUNT, |
| DEFAULT_ACCOUNT_TYPE), |
| "_id=" + eventId, null /* selectionArgs */)); |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null, 2); |
| // Right account, should be deleted |
| assertEquals("delete", 1, mResolver.delete( |
| updatedUri(CalendarContract.Events.CONTENT_URI, true /* syncAdapter */, |
| "user2@google.com", DEFAULT_ACCOUNT_TYPE), |
| "_id=" + eventId, null /* selectionArgs */)); |
| testQueryCount(CalendarContract.Events.CONTENT_URI, null, 1); |
| } |
| |
| /** |
| * Run commands, wiping instance table at each step. |
| * This tests full instance expansion. |
| * @throws Exception |
| */ |
| public void testCommandSequences1() throws Exception { |
| commandSequences(true); |
| } |
| |
| /** |
| * Run commands normally. |
| * This tests incremental instance expansion. |
| * @throws Exception |
| */ |
| public void testCommandSequences2() throws Exception { |
| commandSequences(false); |
| } |
| |
| /** |
| * Run thorough set of command sequences |
| * @param wipe true if instances should be wiped and regenerated |
| * @throws Exception |
| */ |
| private void commandSequences(boolean wipe) throws Exception { |
| Cursor cursor; |
| Uri url = null; |
| mWipe = wipe; // Set global flag |
| |
| mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| cursor = mResolver.query(mEventsUri, null, null, null, null); |
| assertEquals(0, cursor.getCount()); |
| cursor.close(); |
| Command[] commands; |
| |
| Log.i(TAG, "Normal insert/delete"); |
| commands = mNormalInsertDelete; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "All-day insert/delete"); |
| commands = mAlldayInsertDelete; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Recurring insert/delete"); |
| commands = mRecurringInsertDelete; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Exception with truncated recurrence"); |
| commands = mExceptionWithTruncatedRecurrence; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Exception with moved recurrence"); |
| commands = mExceptionWithMovedRecurrence; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Exception with cancel"); |
| commands = mCancelInstance; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Exception with moved recurrence2"); |
| commands = mExceptionWithMovedRecurrence2; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| |
| deleteAllEvents(); |
| |
| Log.i(TAG, "Exception with no recurrence"); |
| commands = mExceptionWithNoRecurrence; |
| for (Command command : commands) { |
| command.execute(); |
| } |
| } |
| |
| /** |
| * Test Time toString. |
| * @throws Exception |
| */ |
| // Suppressed because toString currently hangs. |
| @Suppress |
| public void testTimeToString() throws Exception { |
| Time time = new Time(Time.TIMEZONE_UTC); |
| String str = "2039-01-01T23:00:00.000Z"; |
| String result = "20390101T230000UTC(0,0,0,-1,0)"; |
| time.parse3339(str); |
| assertEquals(result, time.toString()); |
| } |
| |
| /** |
| * Test the query done by Event.loadEvents |
| * Also test that instance queries work when an event straddles the expansion range |
| * @throws Exception |
| */ |
| public void testInstanceQuery() throws Exception { |
| final String[] PROJECTION = new String[] { |
| Instances.TITLE, // 0 |
| Instances.EVENT_LOCATION, // 1 |
| Instances.ALL_DAY, // 2 |
| Instances.CALENDAR_COLOR, // 3 |
| Instances.EVENT_TIMEZONE, // 4 |
| Instances.EVENT_ID, // 5 |
| Instances.BEGIN, // 6 |
| Instances.END, // 7 |
| Instances._ID, // 8 |
| Instances.START_DAY, // 9 |
| Instances.END_DAY, // 10 |
| Instances.START_MINUTE, // 11 |
| Instances.END_MINUTE, // 12 |
| Instances.HAS_ALARM, // 13 |
| Instances.RRULE, // 14 |
| Instances.RDATE, // 15 |
| Instances.SELF_ATTENDEE_STATUS, // 16 |
| Events.ORGANIZER, // 17 |
| Events.GUESTS_CAN_MODIFY, // 18 |
| }; |
| |
| String orderBy = CalendarProvider2.SORT_CALENDAR_VIEW; |
| String where = Instances.SELF_ATTENDEE_STATUS + "!=" |
| + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED; |
| |
| int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| final String START = "2008-05-01T00:00:00"; |
| final String END = "2008-05-01T20:00:00"; |
| |
| EventInfo[] events = { new EventInfo("normal0", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE) }; |
| |
| insertEvent(calId, events[0]); |
| |
| Time time = new Time(DEFAULT_TIMEZONE); |
| time.parse3339(START); |
| long startMs = time.toMillis(true /* ignoreDst */); |
| // Query starting from way in the past to one hour into the event. |
| // Query is more than 2 months so the range won't get extended by the provider. |
| Cursor cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, startMs + DateUtils.HOUR_IN_MILLIS, |
| where, null, orderBy); |
| try { |
| assertEquals(1, cursor.getCount()); |
| } finally { |
| cursor.close(); |
| } |
| |
| // Now expand the instance range. The event overlaps the new part of the range. |
| cursor = queryInstances(mResolver, PROJECTION, |
| startMs - DateUtils.YEAR_IN_MILLIS, startMs + 2 * DateUtils.HOUR_IN_MILLIS, |
| where, null, orderBy); |
| try { |
| assertEquals(1, cursor.getCount()); |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * Performs a query to return all visible instances in the given range that |
| * match the given selection. This is a blocking function and should not be |
| * done on the UI thread. This will cause an expansion of recurring events |
| * to fill this time range if they are not already expanded and will slow |
| * down for larger time ranges with many recurring events. |
| * |
| * @param cr The ContentResolver to use for the query |
| * @param projection The columns to return |
| * @param begin The start of the time range to query in UTC millis since |
| * epoch |
| * @param end The end of the time range to query in UTC millis since epoch |
| * @param selection Filter on the query as an SQL WHERE statement |
| * @param selectionArgs Args to replace any '?'s in the selection |
| * @param orderBy How to order the rows as an SQL ORDER BY statement |
| * @return A Cursor of instances matching the selection |
| */ |
| private static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin, |
| long end, String selection, String[] selectionArgs, String orderBy) { |
| |
| Uri.Builder builder = Instances.CONTENT_URI.buildUpon(); |
| ContentUris.appendId(builder, begin); |
| ContentUris.appendId(builder, end); |
| if (TextUtils.isEmpty(selection)) { |
| selection = WHERE_CALENDARS_SELECTED; |
| selectionArgs = WHERE_CALENDARS_ARGS; |
| } else { |
| selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; |
| if (selectionArgs != null && selectionArgs.length > 0) { |
| selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); |
| selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0]; |
| } else { |
| selectionArgs = WHERE_CALENDARS_ARGS; |
| } |
| } |
| return cr.query(builder.build(), projection, selection, selectionArgs, |
| orderBy == null ? DEFAULT_SORT_ORDER : orderBy); |
| } |
| |
| /** |
| * Performs a query to return all visible instances in the given range that |
| * match the given selection. This is a blocking function and should not be |
| * done on the UI thread. This will cause an expansion of recurring events |
| * to fill this time range if they are not already expanded and will slow |
| * down for larger time ranges with many recurring events. |
| * |
| * @param cr The ContentResolver to use for the query |
| * @param projection The columns to return |
| * @param begin The start of the time range to query in UTC millis since |
| * epoch |
| * @param end The end of the time range to query in UTC millis since epoch |
| * @param searchQuery A string of space separated search terms. Segments |
| * enclosed by double quotes will be treated as a single term. |
| * @param selection Filter on the query as an SQL WHERE statement |
| * @param selectionArgs Args to replace any '?'s in the selection |
| * @param orderBy How to order the rows as an SQL ORDER BY statement |
| * @return A Cursor of instances matching the selection |
| */ |
| public static final Cursor queryInstances(ContentResolver cr, String[] projection, long begin, |
| long end, String searchQuery, String selection, String[] selectionArgs, String orderBy) |
| { |
| Uri.Builder builder = Instances.CONTENT_SEARCH_URI.buildUpon(); |
| ContentUris.appendId(builder, begin); |
| ContentUris.appendId(builder, end); |
| builder = builder.appendPath(searchQuery); |
| if (TextUtils.isEmpty(selection)) { |
| selection = WHERE_CALENDARS_SELECTED; |
| selectionArgs = WHERE_CALENDARS_ARGS; |
| } else { |
| selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; |
| if (selectionArgs != null && selectionArgs.length > 0) { |
| selectionArgs = Arrays.copyOf(selectionArgs, selectionArgs.length + 1); |
| selectionArgs[selectionArgs.length - 1] = WHERE_CALENDARS_ARGS[0]; |
| } else { |
| selectionArgs = WHERE_CALENDARS_ARGS; |
| } |
| } |
| return cr.query(builder.build(), projection, selection, selectionArgs, |
| orderBy == null ? DEFAULT_SORT_ORDER : orderBy); |
| } |
| |
| private Cursor queryInstances(long begin, long end) { |
| Uri url = Uri.withAppendedPath(CalendarContract.Instances.CONTENT_URI, begin + "/" + end); |
| return mResolver.query(url, null, null, null, null); |
| } |
| |
| protected static class MockProvider extends ContentProvider { |
| |
| private String mAuthority; |
| |
| private int mNumItems = 0; |
| |
| public MockProvider(String authority) { |
| mAuthority = authority; |
| } |
| |
| @Override |
| public boolean onCreate() { |
| return true; |
| } |
| |
| @Override |
| public Cursor query(Uri uri, String[] projection, String selection, |
| String[] selectionArgs, String sortOrder) { |
| return new MatrixCursor(new String[]{ "_id" }, 0); |
| } |
| |
| @Override |
| public String getType(Uri uri) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public Uri insert(Uri uri, ContentValues values) { |
| mNumItems++; |
| return Uri.parse("content://" + mAuthority + "/" + mNumItems); |
| } |
| |
| @Override |
| public int delete(Uri uri, String selection, String[] selectionArgs) { |
| return 0; |
| } |
| |
| @Override |
| public int update(Uri uri, ContentValues values, String selection, |
| String[] selectionArgs) { |
| return 0; |
| } |
| } |
| |
| private void cleanCalendarDataTable(SQLiteOpenHelper helper) { |
| if (null == helper) { |
| return; |
| } |
| SQLiteDatabase db = helper.getWritableDatabase(); |
| db.execSQL("DELETE FROM CalendarCache;"); |
| } |
| |
| public void testGetAndSetTimezoneDatabaseVersion() throws CalendarCache.CacheException { |
| CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); |
| cleanCalendarDataTable(helper); |
| CalendarCache cache = new CalendarCache(helper); |
| |
| boolean hasException = false; |
| try { |
| String value = cache.readData(null); |
| } catch (CalendarCache.CacheException e) { |
| hasException = true; |
| } |
| assertTrue(hasException); |
| |
| assertNull(cache.readTimezoneDatabaseVersion()); |
| |
| cache.writeTimezoneDatabaseVersion("1234"); |
| assertEquals("1234", cache.readTimezoneDatabaseVersion()); |
| |
| cache.writeTimezoneDatabaseVersion("5678"); |
| assertEquals("5678", cache.readTimezoneDatabaseVersion()); |
| } |
| |
| private void checkEvent(int eventId, String title, long dtStart, long dtEnd, boolean allDay) { |
| Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/events"); |
| Log.i(TAG, "Looking for EventId = " + eventId); |
| |
| Cursor cursor = mResolver.query(uri, null, null, null, null); |
| assertEquals(1, cursor.getCount()); |
| |
| int colIndexTitle = cursor.getColumnIndex(CalendarContract.Events.TITLE); |
| int colIndexDtStart = cursor.getColumnIndex(CalendarContract.Events.DTSTART); |
| int colIndexDtEnd = cursor.getColumnIndex(CalendarContract.Events.DTEND); |
| int colIndexAllDay = cursor.getColumnIndex(CalendarContract.Events.ALL_DAY); |
| if (!cursor.moveToNext()) { |
| Log.e(TAG,"Could not find inserted event"); |
| assertTrue(false); |
| } |
| assertEquals(title, cursor.getString(colIndexTitle)); |
| assertEquals(dtStart, cursor.getLong(colIndexDtStart)); |
| assertEquals(dtEnd, cursor.getLong(colIndexDtEnd)); |
| assertEquals(allDay, (cursor.getInt(colIndexAllDay) != 0)); |
| cursor.close(); |
| } |
| |
| public void testChangeTimezoneDB() { |
| int calId = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| Cursor cursor = mResolver |
| .query(CalendarContract.Events.CONTENT_URI, null, null, null, null); |
| assertEquals(0, cursor.getCount()); |
| cursor.close(); |
| |
| EventInfo[] events = { new EventInfo("normal0", |
| "2008-05-01T00:00:00", |
| "2008-05-02T00:00:00", |
| false, |
| DEFAULT_TIMEZONE) }; |
| |
| Uri uri = insertEvent(calId, events[0]); |
| assertNotNull(uri); |
| |
| // check the inserted event |
| checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay); |
| |
| // inject a new time zone |
| getProvider().doProcessEventRawTimes(TIME_ZONE_AMERICA_ANCHORAGE, |
| MOCK_TIME_ZONE_DATABASE_VERSION); |
| |
| // check timezone database version |
| assertEquals(MOCK_TIME_ZONE_DATABASE_VERSION, getProvider().getTimezoneDatabaseVersion()); |
| |
| // check that the inserted event has *not* been updated |
| checkEvent(1, events[0].mTitle, events[0].mDtstart, events[0].mDtend, events[0].mAllDay); |
| } |
| |
| public static final Uri PROPERTIES_CONTENT_URI = |
| Uri.parse("content://" + CalendarContract.AUTHORITY + "/properties"); |
| |
| public void testGetProviderProperties() throws CalendarCache.CacheException { |
| CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); |
| cleanCalendarDataTable(helper); |
| CalendarCache cache = new CalendarCache(helper); |
| |
| cache.writeTimezoneDatabaseVersion("2010k"); |
| cache.writeTimezoneInstances("America/Denver"); |
| cache.writeTimezoneInstancesPrevious("America/Los_Angeles"); |
| cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); |
| |
| Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, null, null, null); |
| assertEquals(4, cursor.getCount()); |
| |
| final int keyColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY); |
| final int valueColumnIndex = cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE); |
| Map<String, String> map = new HashMap<String, String>(); |
| |
| while (cursor.moveToNext()) { |
| String key = cursor.getString(keyColumnIndex); |
| String value = cursor.getString(valueColumnIndex); |
| map.put(key, value); |
| } |
| |
| assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION)); |
| assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_TYPE)); |
| assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES)); |
| assertTrue(map.containsKey(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)); |
| |
| assertEquals("2010k", map.get(CalendarCache.KEY_TIMEZONE_DATABASE_VERSION)); |
| assertEquals("America/Denver", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES)); |
| assertEquals("America/Los_Angeles", map.get(CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS)); |
| assertEquals(CalendarCache.TIMEZONE_TYPE_AUTO, map.get(CalendarCache.KEY_TIMEZONE_TYPE)); |
| |
| cursor.close(); |
| } |
| |
| public void testGetProviderPropertiesByKey() throws CalendarCache.CacheException { |
| CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); |
| cleanCalendarDataTable(helper); |
| CalendarCache cache = new CalendarCache(helper); |
| |
| cache.writeTimezoneDatabaseVersion("2010k"); |
| cache.writeTimezoneInstances("America/Denver"); |
| cache.writeTimezoneInstancesPrevious("America/Los_Angeles"); |
| cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); |
| |
| checkValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE); |
| checkValueForKey("2010k", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); |
| checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES); |
| checkValueForKey("America/Los_Angeles", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); |
| } |
| |
| private void checkValueForKey(String value, String key) { |
| Cursor cursor = mResolver.query(PROPERTIES_CONTENT_URI, null, |
| "key=?", new String[] {key}, null); |
| |
| assertEquals(1, cursor.getCount()); |
| assertTrue(cursor.moveToFirst()); |
| assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_KEY)), |
| key); |
| assertEquals(cursor.getString(cursor.getColumnIndex(CalendarCache.COLUMN_NAME_VALUE)), |
| value); |
| |
| cursor.close(); |
| } |
| |
| public void testUpdateProviderProperties() throws CalendarCache.CacheException { |
| CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper(); |
| cleanCalendarDataTable(helper); |
| CalendarCache cache = new CalendarCache(helper); |
| |
| String localTimezone = TimeZone.getDefault().getID(); |
| |
| // Set initial value |
| cache.writeTimezoneDatabaseVersion("2010k"); |
| |
| updateValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); |
| checkValueForKey("2009s", CalendarCache.KEY_TIMEZONE_DATABASE_VERSION); |
| |
| // Set initial values |
| cache.writeTimezoneType(CalendarCache.TIMEZONE_TYPE_AUTO); |
| cache.writeTimezoneInstances("America/Chicago"); |
| cache.writeTimezoneInstancesPrevious("America/Denver"); |
| |
| updateValueForKey(CalendarCache.TIMEZONE_TYPE_AUTO, CalendarCache.KEY_TIMEZONE_TYPE); |
| checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); |
| checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); |
| |
| updateValueForKey(CalendarCache.TIMEZONE_TYPE_HOME, CalendarCache.KEY_TIMEZONE_TYPE); |
| checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES); |
| checkValueForKey("America/Denver", CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); |
| |
| // Set initial value |
| cache.writeTimezoneInstancesPrevious(""); |
| updateValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); |
| checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES); |
| checkValueForKey(localTimezone, CalendarCache.KEY_TIMEZONE_INSTANCES_PREVIOUS); |
| } |
| |
| private void updateValueForKey(String value, String key) { |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(CalendarCache.COLUMN_NAME_VALUE, value); |
| |
| int result = mResolver.update(PROPERTIES_CONTENT_URI, |
| contentValues, |
| CalendarCache.COLUMN_NAME_KEY + "=?", |
| new String[] {key}); |
| |
| assertEquals(1, result); |
| } |
| |
| /** |
| * Verifies that the number of defined calendars meets expectations. |
| * |
| * @param expectedCount The number of calendars we expect to find. |
| */ |
| private void checkCalendarCount(int expectedCount) { |
| Cursor cursor = mResolver.query(mCalendarsUri, |
| null /* projection */, |
| null /* selection */, |
| null /* selectionArgs */, |
| null /* sortOrder */); |
| assertEquals(expectedCount, cursor.getCount()); |
| cursor.close(); |
| } |
| |
| private void checkCalendarExists(int calId) { |
| assertTrue(isCalendarExists(calId)); |
| } |
| |
| private void checkCalendarDoesNotExists(int calId) { |
| assertFalse(isCalendarExists(calId)); |
| } |
| |
| private boolean isCalendarExists(int calId) { |
| Cursor cursor = mResolver.query(mCalendarsUri, |
| new String[] {Calendars._ID}, |
| null /* selection */, |
| null /* selectionArgs */, |
| null /* sortOrder */); |
| boolean found = false; |
| while (cursor.moveToNext()) { |
| if (calId == cursor.getInt(0)) { |
| found = true; |
| break; |
| } |
| } |
| cursor.close(); |
| return found; |
| } |
| |
| public void testDeleteAllCalendars() { |
| checkCalendarCount(0); |
| |
| insertCal("Calendar1", "America/Los_Angeles"); |
| insertCal("Calendar2", "America/Los_Angeles"); |
| |
| checkCalendarCount(2); |
| |
| deleteMatchingCalendars(null /* selection */, null /* selectionArgs*/); |
| checkCalendarCount(0); |
| } |
| |
| public void testDeleteCalendarsWithSelection() { |
| checkCalendarCount(0); |
| |
| int calId1 = insertCal("Calendar1", "America/Los_Angeles"); |
| int calId2 = insertCal("Calendar2", "America/Los_Angeles"); |
| |
| checkCalendarCount(2); |
| checkCalendarExists(calId1); |
| checkCalendarExists(calId2); |
| |
| deleteMatchingCalendars(Calendars._ID + "=" + calId2, null /* selectionArgs*/); |
| checkCalendarCount(1); |
| checkCalendarExists(calId1); |
| checkCalendarDoesNotExists(calId2); |
| } |
| |
| public void testDeleteCalendarsWithSelectionAndArgs() { |
| checkCalendarCount(0); |
| |
| int calId1 = insertCal("Calendar1", "America/Los_Angeles"); |
| int calId2 = insertCal("Calendar2", "America/Los_Angeles"); |
| |
| checkCalendarCount(2); |
| checkCalendarExists(calId1); |
| checkCalendarExists(calId2); |
| |
| deleteMatchingCalendars(Calendars._ID + "=?", |
| new String[] { Integer.toString(calId2) }); |
| checkCalendarCount(1); |
| checkCalendarExists(calId1); |
| checkCalendarDoesNotExists(calId2); |
| |
| deleteMatchingCalendars(Calendars._ID + "=?" + " AND " + Calendars.NAME + "=?", |
| new String[] { Integer.toString(calId1), "Calendar1" }); |
| checkCalendarCount(0); |
| } |
| |
| public void testGetColumnIndex_IsPrimary() { |
| checkCalendarCount(0); |
| int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| String[] projection = new String[] { |
| Calendars.ACCOUNT_NAME, |
| Calendars.CALENDAR_DISPLAY_NAME, |
| Calendars.OWNER_ACCOUNT, |
| Calendars.IS_PRIMARY |
| }; |
| String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; |
| String[] selectionArgs = new String[] { |
| DEFAULT_ACCOUNT |
| }; |
| Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, |
| null); |
| assertNotNull(cursor); |
| assertEquals(3, cursor.getColumnIndex(Calendars.IS_PRIMARY)); |
| cursor.close(); |
| deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); |
| checkCalendarCount(0); |
| } |
| |
| public void testGetIsPrimary_ForEvents() { |
| checkCalendarCount(0); |
| int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| final String START = "2008-05-01T00:00:00"; |
| final String END = "2008-05-01T20:00:00"; |
| EventInfo event = new EventInfo("search orange", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE); |
| |
| insertEvent(calendarId0, event); |
| |
| String[] projection = new String[] { |
| Calendars.IS_PRIMARY |
| }; |
| String selection = |
| "((" + Calendars.IS_PRIMARY + " = ? OR " + Calendars.ACCOUNT_NAME + " = ?))"; |
| String[] selectionArgs = new String[] { |
| "1", DEFAULT_ACCOUNT |
| }; |
| Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, |
| null); |
| assertNotNull(cursor); |
| cursor.moveToLast(); |
| assertEquals(1, cursor.getCount()); |
| assertEquals(1, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); |
| cursor.close(); |
| deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); |
| checkCalendarCount(0); |
| } |
| |
| public void testGetIsNotPrimary_ForEvents() { |
| checkCalendarCount(0); |
| int calendarId0 = insertNonPrimaryCal("Calendar0", DEFAULT_TIMEZONE, DEFAULT_ACCOUNT); |
| |
| final String START = "2008-05-01T00:00:00"; |
| final String END = "2008-05-01T20:00:00"; |
| EventInfo event = new EventInfo("search orange", |
| START, |
| END, |
| false /* allDay */, |
| DEFAULT_TIMEZONE); |
| |
| insertEvent(calendarId0, event); |
| |
| String[] projection = new String[] { |
| Calendars.IS_PRIMARY |
| }; |
| String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; |
| String[] selectionArgs = new String[] { |
| DEFAULT_ACCOUNT |
| }; |
| Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, |
| null); |
| assertNotNull(cursor); |
| cursor.moveToLast(); |
| assertEquals(1, cursor.getCount()); |
| assertEquals(0, cursor.getInt(cursor.getColumnIndex(Calendars.IS_PRIMARY))); |
| cursor.close(); |
| deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); |
| checkCalendarCount(0); |
| } |
| |
| public void testGetColumnIndex_Count() { |
| checkCalendarCount(0); |
| int calendarId0 = insertCal("Calendar0", DEFAULT_TIMEZONE); |
| |
| String[] projection = new String[] { |
| BaseColumns._COUNT |
| }; |
| String selection = "((" + Calendars.ACCOUNT_NAME + " = ? ))"; |
| String[] selectionArgs = new String[] { |
| DEFAULT_ACCOUNT |
| }; |
| Cursor cursor = mResolver.query(Calendars.CONTENT_URI, projection, selection, selectionArgs, |
| null); |
| assertNotNull(cursor); |
| assertEquals(0, cursor.getColumnIndex(BaseColumns._COUNT)); |
| cursor.close(); |
| deleteMatchingCalendars(Calendars._ID + "=" + calendarId0, null /* selectionArgs*/); |
| checkCalendarCount(0); |
| } |
| |
| public void testEnterpriseInstancesGetCorrectValue() { |
| final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| insertWorkEvent(WORK_EVENT_TITLE, calendarId, |
| WORK_EVENT_DTSTART, WORK_EVENT_DTEND); |
| insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, |
| WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| Uri.Builder builder = Instances.ENTERPRISE_CONTENT_URI.buildUpon(); |
| ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS); |
| ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); |
| String[] projection = new String[]{ |
| Instances.TITLE, |
| Instances.CALENDAR_ID, |
| Instances.DTSTART, |
| Instances.CALENDAR_COLOR, |
| }; |
| Cursor cursor = mResolver.query( |
| builder.build(), |
| projection, null, null, null); |
| |
| // Test the return cursor is correct when the all checks are met. |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(WORK_EVENT_TITLE, cursor.getString(0)); |
| assertEquals(calendarId, cursor.getLong(1)); |
| assertEquals(WORK_EVENT_DTSTART, cursor.getLong(2)); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(3)); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForEvents(calendarId, 2); |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| public void testEnterpriseInstancesContentSearch() { |
| final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| insertWorkEvent(WORK_EVENT_TITLE, calendarId, |
| WORK_EVENT_DTSTART, WORK_EVENT_DTEND); |
| insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, |
| WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| Uri.Builder builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon(); |
| ContentUris.appendId(builder, WORK_EVENT_DTSTART - DateUtils.YEAR_IN_MILLIS); |
| ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); |
| builder = builder.appendPath(WORK_EVENT_TITLE /* search query */); |
| Cursor cursor = mResolver.query( |
| builder.build(), |
| null, null, null, null); |
| // There is only one event that meets the search criteria. |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| |
| builder = Instances.ENTERPRISE_CONTENT_SEARCH_URI.buildUpon(); |
| ContentUris.appendId(builder, WORK_EVENT_DTSTART_STANDBY - DateUtils.YEAR_IN_MILLIS); |
| ContentUris.appendId(builder, WORK_EVENT_DTEND + DateUtils.YEAR_IN_MILLIS); |
| builder = builder.appendPath(WORK_EVENT_DESCRIPTION /* search query */); |
| cursor = mResolver.query( |
| builder.build(), |
| null, null, null, null); |
| // There are two events that meet the search criteria. |
| assertNotNull(cursor); |
| assertEquals(2, cursor.getCount()); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForEvents(calendarId, 2); |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| public void testEnterpriseEventsGetCorrectValue() { |
| final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, |
| WORK_EVENT_DTSTART, WORK_EVENT_DTEND); |
| insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, |
| WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| String selection = "(" + Events.TITLE + " = ? )"; |
| String[] selectionArgs = new String[]{ |
| WORK_EVENT_TITLE |
| }; |
| String[] projection = new String[]{ |
| Events._ID, |
| Events.TITLE, |
| Events.CALENDAR_ID, |
| Events.DTSTART, |
| Calendars.CALENDAR_COLOR |
| }; |
| Cursor cursor = mResolver.query( |
| Events.ENTERPRISE_CONTENT_URI, |
| projection, selection, selectionArgs, null); |
| |
| // Test the return cursor is correct when the all checks are met. |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(idToTest, cursor.getLong(0)); |
| assertEquals(WORK_EVENT_TITLE, cursor.getString(1)); |
| assertEquals(calendarId, cursor.getLong(2)); |
| assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3)); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4)); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForEvents(calendarId, 2); |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| public void testEnterpriseEventsGetCorrectValueWithId() { |
| final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, |
| WORK_EVENT_DTSTART, WORK_EVENT_DTEND); |
| insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, |
| WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| // Test ENTERPRISE_CONTENT_URI_ID. |
| String[] projection = new String[]{ |
| Events._ID, |
| Events.TITLE, |
| Events.CALENDAR_ID, |
| Events.DTSTART, |
| Calendars.CALENDAR_COLOR |
| }; |
| final Cursor cursor = mResolver.query( |
| ContentUris.withAppendedId(Events.ENTERPRISE_CONTENT_URI, idToTest), |
| projection, null, null, null); |
| |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(idToTest, cursor.getLong(0)); |
| assertEquals(WORK_EVENT_TITLE, cursor.getString(1)); |
| assertEquals(calendarId, cursor.getLong(2)); |
| assertEquals(WORK_EVENT_DTSTART, cursor.getLong(3)); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(4)); |
| assertEquals(1, cursor.getInt(2)); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForEvents(calendarId, 2); |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| public void testEnterpriseEventsProjectionCalibration() { |
| final long calendarId = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| final long idToTest = insertWorkEvent(WORK_EVENT_TITLE, calendarId, |
| WORK_EVENT_DTSTART, WORK_EVENT_DTEND); |
| insertWorkEvent(WORK_EVENT_TITLE_STANDBY, calendarId, |
| WORK_EVENT_DTSTART_STANDBY, WORK_EVENT_DTEND_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| // Test all whitelisted columns are returned when projection is empty. |
| String selection = "(" + Events.TITLE + " = ? )"; |
| String[] selectionArgs = new String[]{ |
| WORK_EVENT_TITLE |
| }; |
| final Cursor cursor = mResolver.query( |
| Events.ENTERPRISE_CONTENT_URI, |
| new String[] {}, selection, selectionArgs, null); |
| |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| for (String column : CrossProfileCalendarHelper.EVENTS_TABLE_WHITELIST) { |
| final int index = cursor.getColumnIndex(column); |
| assertTrue(index != -1); |
| } |
| assertEquals(idToTest, cursor.getLong( |
| cursor.getColumnIndex(Events._ID))); |
| assertEquals(calendarId, cursor.getLong( |
| cursor.getColumnIndex(Events.CALENDAR_ID))); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt( |
| cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); |
| assertEquals(1, cursor.getInt( |
| cursor.getColumnIndex(Calendars.IS_PRIMARY))); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForEvents(calendarId, 2); |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| private void cleanupEnterpriseTestForEvents(long calendarId, int numToDelete) { |
| // Selection arguments must be provided when deleting events. |
| final int numDeleted = mWorkProfileProvider.delete(Events.CONTENT_URI, |
| "(" + Events.CALENDAR_ID + " = ? )", |
| new String[]{String.valueOf(calendarId)}); |
| assertTrue(numDeleted == numToDelete); |
| } |
| |
| private long insertWorkEvent(String eventTitle, long calendarId, long dtStart, long dtEnd) { |
| final ContentValues cv = new ContentValues(); |
| cv.put(Events.TITLE, eventTitle); |
| cv.put(Events.CALENDAR_ID, calendarId); |
| cv.put(Events.DESCRIPTION, WORK_EVENT_DESCRIPTION); |
| cv.put(Events.EVENT_LOCATION, WORK_EVENT_LOCATION); |
| cv.put(Events.EVENT_COLOR, WORK_EVENT_COLOR); |
| cv.put(Events.DTSTART, dtStart); |
| cv.put(Events.DTEND, dtEnd); |
| cv.put(Events.EVENT_TIMEZONE, WORK_DEFAULT_TIMEZONE); |
| final Uri uri = mWorkProfileProvider.insert(Events.CONTENT_URI, cv); |
| return Long.parseLong(uri.getLastPathSegment()); |
| } |
| |
| public void testEnterpriseCalendarGetCorrectValue() { |
| final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| // Test the return cursor is correct when the all checks are met. |
| String selection = "(" + Calendars.CALENDAR_DISPLAY_NAME + " = ? )"; |
| String[] selectionArgs = new String[] { |
| WORK_CALENDAR_TITLE |
| }; |
| String[] projection = new String[] { |
| Calendars._ID, |
| Calendars.CALENDAR_COLOR |
| }; |
| Cursor cursor = mResolver.query( |
| Calendars.ENTERPRISE_CONTENT_URI, |
| projection, selection, selectionArgs, null); |
| |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(idToTest, cursor.getLong(0)); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1)); |
| |
| cleanupEnterpriseTestForCalendars(2); |
| } |
| |
| public void testEnterpriseCalendarGetCorrectValueWithId() { |
| final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| insertWorkCalendar(WORK_CALENDAR_TITLE_STANDBY); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| // Test Calendars.ENTERPRISE_CONTENT_URI with id. |
| String[] projection = new String[] { |
| Calendars._ID, |
| Calendars.CALENDAR_COLOR |
| }; |
| final Cursor cursor = mResolver.query( |
| ContentUris.withAppendedId(Calendars.ENTERPRISE_CONTENT_URI, idToTest), |
| projection, null, null, null); |
| |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| assertEquals(idToTest, cursor.getLong(0)); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt(1)); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForCalendars(2); |
| } |
| |
| public void testEnterpriseCalendarsProjectionCalibration() { |
| final long idToTest = insertWorkCalendar(WORK_CALENDAR_TITLE); |
| // Assume cross profile uri access is allowed by policy and settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(true); |
| |
| // Test all whitelisted columns are returned when projection is empty. |
| final Cursor cursor = mResolver.query( |
| Calendars.ENTERPRISE_CONTENT_URI, |
| new String[] {}, null, null, null); |
| |
| assertNotNull(cursor); |
| assertEquals(1, cursor.getCount()); |
| cursor.moveToFirst(); |
| for (String column : CrossProfileCalendarHelper.CALENDARS_TABLE_WHITELIST) { |
| final int index = cursor.getColumnIndex(column); |
| assertTrue(index != -1); |
| } |
| assertEquals(idToTest, cursor.getLong( |
| cursor.getColumnIndex(Calendars._ID))); |
| assertEquals(WORK_CALENDAR_COLOR, cursor.getInt( |
| cursor.getColumnIndex(Calendars.CALENDAR_COLOR))); |
| cursor.close(); |
| |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| public void testEnterpriseCalendarsNonWhitelistedProjection() { |
| // Test SecurityException is thrown there is non-whitelisted column in the projection. |
| try { |
| String[] projection = new String[] { |
| Calendars._ID, |
| Calendars.CALENDAR_DISPLAY_NAME, |
| Calendars.CALENDAR_COLOR, |
| Calendars.OWNER_ACCOUNT |
| }; |
| mResolver.query( |
| Calendars.ENTERPRISE_CONTENT_URI, |
| projection, null, null, null); |
| fail("IllegalArgumentException is not thrown when querying non-whitelisted columns"); |
| } catch (IllegalArgumentException e) { |
| } |
| } |
| |
| public void testEnterpriseCalendarsNotAllowed() { |
| insertWorkCalendar(WORK_CALENDAR_TITLE); |
| // Assume cross profile uri access is not allowed by policy or disabled in settings. |
| MockCrossProfileCalendarHelper.setPackageAllowedToAccessCalendar(false); |
| |
| // Throw exception if cross profile calendar is disabled in settings. |
| try { |
| final Cursor cursor = mResolver.query( |
| Calendars.ENTERPRISE_CONTENT_URI, |
| new String[]{}, null, null, null); |
| fail("Unsupported operation exception should have been raised."); |
| } catch (UnsupportedOperationException e) { |
| // Exception expected. |
| } |
| cleanupEnterpriseTestForCalendars(1); |
| } |
| |
| // Remove the two inserted calendars. |
| private void cleanupEnterpriseTestForCalendars(int numToDelete) { |
| final int numDeleted = mWorkProfileProvider.delete(Calendars.CONTENT_URI, null, null); |
| assertTrue(numDeleted == numToDelete); |
| } |
| |
| private long insertWorkCalendar(String displayName) { |
| final ContentValues cv = new ContentValues(); |
| cv.put(Calendars.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE); |
| cv.put(Calendars.OWNER_ACCOUNT, DEFAULT_ACCOUNT); |
| cv.put(Calendars.ACCOUNT_NAME, DEFAULT_ACCOUNT); |
| cv.put(Calendars.CALENDAR_DISPLAY_NAME, displayName); |
| cv.put(Calendars.CALENDAR_COLOR, WORK_CALENDAR_COLOR); |
| cv.put(Calendars.CALENDAR_TIME_ZONE, WORK_DEFAULT_TIMEZONE); |
| final Uri uri = mWorkProfileProvider.insert( |
| addSyncQueryParams(Calendars.CONTENT_URI, "local_account", |
| CalendarContract.ACCOUNT_TYPE_LOCAL), cv); |
| return Long.parseLong(uri.getLastPathSegment()); |
| } |
| } |
| |