am 62cf0ad5: am 2306a400: reconcile main tree with open-source eclair
Merge commit '62cf0ad5bcb18bc85f17b30296882b109b05b290'
* commit '62cf0ad5bcb18bc85f17b30296882b109b05b290':
android-2.1_r1 snapshot
diff --git a/Android.mk b/Android.mk
index 3d7c95c..a9278de 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,13 +16,12 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := user
+LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under,src)
-# We depend on googlelogin-client also, but that is already
-# being included by google-framework
-LOCAL_STATIC_JAVA_LIBRARIES := google-framework
+LOCAL_STATIC_JAVA_LIBRARIES := gsf-client # Google apps only: GSF client libraries
+LOCAL_STATIC_JAVA_LIBRARIES += google-common # Google apps only: shared Google code
LOCAL_PACKAGE_NAME := CalendarProvider
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7595b1c..c17ef2c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4,9 +4,9 @@
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.
@@ -39,18 +39,13 @@
<!-- TODO: Remove dependency of application on the test runner
(android.test) library. -->
<uses-library android:name="android.test.runner" />
- <provider android:name="CalendarProvider" android:authorities="calendar"
+
+ <provider android:name="CalendarProvider2" android:authorities="com.android.calendar"
android:label="@string/provider_label"
android:multiprocess="false"
android:readPermission="android.permission.READ_CALENDAR"
android:writePermission="android.permission.WRITE_CALENDAR" />
- <service android:name="CalendarSyncAdapterService" android:exported="true">
- <intent-filter>
- <action android:name="android.content.SyncAdapter" />
- </intent-filter>
- <meta-data android:name="android.content.SyncAdapter"
- android:resource="@xml/syncadapter" />
- </service>
+
<activity android:name="CalendarContentProviderTests" android:label="Calendar Content Provider">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/res/layout-land/agenda_appwidget.xml b/res/layout-land/agenda_appwidget.xml
index eb9734b..f4a2276 100644
--- a/res/layout-land/agenda_appwidget.xml
+++ b/res/layout-land/agenda_appwidget.xml
@@ -16,7 +16,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/agenda_appwidget"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/appwidget_background"
android:focusable="true"
@@ -24,7 +24,7 @@
<ImageView
android:id="@+id/divider"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
@@ -34,8 +34,8 @@
<TextView
android:id="@+id/no_events"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:padding="10dip"
android:gravity="center"
android:textSize="14sp"
@@ -92,7 +92,7 @@
<TextView
android:id="@+id/where"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:paddingBottom="6dip"
@@ -123,7 +123,7 @@
<TextView
android:id="@+id/title"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/when"
android:layout_above="@id/where"
diff --git a/res/layout/agenda_appwidget.xml b/res/layout/agenda_appwidget.xml
index 503fe18..af4a057 100644
--- a/res/layout/agenda_appwidget.xml
+++ b/res/layout/agenda_appwidget.xml
@@ -16,15 +16,15 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/agenda_appwidget"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:background="@drawable/appwidget_background"
android:focusable="true"
android:clickable="true">
<ImageView
android:id="@+id/divider"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
@@ -34,8 +34,8 @@
<TextView
android:id="@+id/no_events"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:padding="10dip"
android:gravity="center"
android:textSize="14sp"
@@ -77,7 +77,7 @@
<TextView
android:id="@+id/when"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/divider"
android:layout_marginTop="6dip"
@@ -92,7 +92,7 @@
<TextView
android:id="@+id/where"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="6dip"
@@ -106,7 +106,7 @@
<TextView
android:id="@+id/conflict"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/where"
android:layout_alignWithParentIfMissing="true"
@@ -121,7 +121,7 @@
<TextView
android:id="@+id/title"
- android:layout_width="fill_parent"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/when"
android:layout_above="@id/conflict"
diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml
index 9036026..c977c14 100644
--- a/res/xml/syncadapter.xml
+++ b/res/xml/syncadapter.xml
@@ -21,6 +21,6 @@
<!-- for the SyncAdapter. -->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
- android:contentAuthority="calendar"
+ android:contentAuthority="com.android.calendar"
android:accountType="com.google"
/>
diff --git a/src/com/android/providers/calendar/CalendarCache.java b/src/com/android/providers/calendar/CalendarCache.java
new file mode 100644
index 0000000..5c94b89
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarCache.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 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.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+/**
+ * Class for managing a persistent Cache of (key, value) pairs. The persistent storage used is
+ * a SQLite database.
+ */
+public class CalendarCache {
+ private static final String TAG = "CalendarCache";
+
+ public static final String DATABASE_NAME = "CalendarCache";
+
+ public static final String KEY_TIMEZONE_DATABASE_VERSION = "timezoneDatabaseVersion";
+ public static final String DEFAULT_TIMEZONE_DATABASE_VERSION = "2009s";
+
+ private static final String COLUMN_NAME_ID = "_id";
+ private static final String COLUMN_NAME_KEY = "key";
+ private static final String COLUMN_NAME_VALUE = "value";
+
+ private static final String[] sProjection = {
+ COLUMN_NAME_KEY,
+ COLUMN_NAME_VALUE
+ };
+
+ private static final int COLUMN_INDEX_KEY = 0;
+ private static final int COLUMN_INDEX_VALUE = 1;
+
+ private final SQLiteOpenHelper mOpenHelper;
+
+ /**
+ * This exception is thrown when the cache encounter a null key or a null database reference
+ */
+ public static class CacheException extends Exception {
+ public CacheException() {
+ }
+
+ public CacheException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ public CalendarCache(SQLiteOpenHelper openHelper) {
+ mOpenHelper = openHelper;
+ }
+
+ public void writeTimezoneDatabaseVersion(String timezoneDatabaseVersion) throws CacheException {
+ writeData(KEY_TIMEZONE_DATABASE_VERSION,
+ timezoneDatabaseVersion);
+ }
+
+ public String readTimezoneDatabaseVersion() throws CacheException {
+ return readData(KEY_TIMEZONE_DATABASE_VERSION);
+ }
+
+ /**
+ * Write a (key, value) pair in the Cache.
+ *
+ * @param key the key (must not be null)
+ * @param value the value (can be null)
+ * @throws CacheException when key is null
+ */
+ public void writeData(String key, String value) throws CacheException {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ db.beginTransaction();
+ try {
+ writeDataLocked(db, key, value);
+ db.setTransactionSuccessful();
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.i(TAG, "Wrote (key, value) = [ " + key + ", " + value + "] ");
+ }
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Write a (key, value) pair in the database used by the cache. This call should be called into
+ * a transaction.
+ *
+ * @param db the database (must not be null)
+ * @param key the key (must not be null)
+ * @param value the value
+ * @throws CacheException when key or database are null
+ */
+ protected void writeDataLocked(SQLiteDatabase db, String key, String value)
+ throws CacheException {
+ if (null == db) {
+ throw new CacheException("Database cannot be null");
+ }
+ if (null == key) {
+ throw new CacheException("Cannot use null key for write");
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_NAME_ID, key.hashCode());
+ values.put(COLUMN_NAME_KEY, key);
+ values.put(COLUMN_NAME_VALUE, value);
+
+ db.replace(DATABASE_NAME, null /* null column hack */, values);
+ }
+
+ /**
+ * Read a value from the database used by the cache and depending on a key.
+ *
+ * @param key the key from which we want the value (must not be null)
+ * @return the value that was found for the key. Can be null if no key has been found
+ * @throws CacheException when key is null
+ */
+ public String readData(String key) throws CacheException {
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ return readDataLocked(db, key);
+ }
+
+ /**
+ * Read a value from the database used by the cache and depending on a key. The database should
+ * be "readable" at minimum
+ *
+ * @param db the database (must not be null)
+ * @param key the key from which we want the value (must not be null)
+ * @return the value that was found for the key. Can be null if no value has been found for the
+ * key.
+ * @throws CacheException when key or database are null
+ */
+ protected String readDataLocked(SQLiteDatabase db, String key) throws CacheException {
+ if (null == db) {
+ throw new CacheException("Database cannot be null");
+ }
+ if (null == key) {
+ throw new CacheException("Cannot use null key for read");
+ }
+
+ String rowValue = null;
+
+ Cursor cursor = db.query(DATABASE_NAME, sProjection,
+ COLUMN_NAME_KEY + "=?", new String[] { key }, null, null, null);
+ try {
+ if (cursor.moveToNext()) {
+ rowValue = cursor.getString(COLUMN_INDEX_VALUE);
+ }
+ else {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.i(TAG, "Could not find key = [ " + key + " ]");
+ }
+ }
+ } finally {
+ cursor.close();
+ cursor = null;
+ }
+ return rowValue;
+ }
+}
diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
new file mode 100644
index 0000000..797a943
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
@@ -0,0 +1,817 @@
+/*
+ * Copyright (C) 2009 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.accounts.Account;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Bundle;
+import android.provider.Calendar;
+import android.provider.ContactsContract;
+import android.util.Log;
+import com.android.internal.content.SyncStateContentProviderHelper;
+
+import java.net.URLDecoder;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Database helper for calendar. Designed as a singleton to make sure that all
+ * {@link android.content.ContentProvider} users get the same reference.
+ */
+/* package */ class CalendarDatabaseHelper extends SQLiteOpenHelper {
+ private static final String TAG = "CalendarDatabaseHelper";
+
+ private static final String DATABASE_NAME = "calendar.db";
+
+ // TODO: change the Calendar contract so these are defined there.
+ static final String ACCOUNT_NAME = "_sync_account";
+ static final String ACCOUNT_TYPE = "_sync_account_type";
+
+ // Note: if you update the version number, you must also update the code
+ // in upgradeDatabase() to modify the database (gracefully, if possible).
+ private static final int DATABASE_VERSION = 60;
+
+ private final Context mContext;
+ private final SyncStateContentProviderHelper mSyncState;
+
+ private static CalendarDatabaseHelper sSingleton = null;
+
+ private DatabaseUtils.InsertHelper mCalendarsInserter;
+ private DatabaseUtils.InsertHelper mEventsInserter;
+ private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
+ private DatabaseUtils.InsertHelper mInstancesInserter;
+ private DatabaseUtils.InsertHelper mAttendeesInserter;
+ private DatabaseUtils.InsertHelper mRemindersInserter;
+ private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
+ private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
+
+ public long calendarsInsert(ContentValues values) {
+ return mCalendarsInserter.insert(values);
+ }
+
+ public long eventsInsert(ContentValues values) {
+ return mEventsInserter.insert(values);
+ }
+
+ public long eventsRawTimesInsert(ContentValues values) {
+ return mEventsRawTimesInserter.insert(values);
+ }
+
+ public long eventsRawTimesReplace(ContentValues values) {
+ return mEventsRawTimesInserter.replace(values);
+ }
+
+ public long instancesInsert(ContentValues values) {
+ return mInstancesInserter.insert(values);
+ }
+
+ public long attendeesInsert(ContentValues values) {
+ return mAttendeesInserter.insert(values);
+ }
+
+ public long remindersInsert(ContentValues values) {
+ return mRemindersInserter.insert(values);
+ }
+
+ public long calendarAlertsInsert(ContentValues values) {
+ return mCalendarAlertsInserter.insert(values);
+ }
+
+ public long extendedPropertiesInsert(ContentValues values) {
+ return mExtendedPropertiesInserter.insert(values);
+ }
+
+ public static synchronized CalendarDatabaseHelper getInstance(Context context) {
+ if (sSingleton == null) {
+ sSingleton = new CalendarDatabaseHelper(context);
+ }
+ return sSingleton;
+ }
+
+ /**
+ * Private constructor, callers except unit tests should obtain an instance through
+ * {@link #getInstance(android.content.Context)} instead.
+ */
+ /* package */ CalendarDatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ if (false) Log.i(TAG, "Creating OpenHelper");
+ Resources resources = context.getResources();
+
+ mContext = context;
+ mSyncState = new SyncStateContentProviderHelper();
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ mSyncState.onDatabaseOpened(db);
+
+ mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
+ mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
+ mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
+ mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
+ mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
+ mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
+ mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
+ mExtendedPropertiesInserter =
+ new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.i(TAG, "Bootstrapping database");
+
+ mSyncState.createDatabase(db);
+
+ db.execSQL("CREATE TABLE Calendars (" +
+ "_id INTEGER PRIMARY KEY," +
+ ACCOUNT_NAME + " TEXT," +
+ ACCOUNT_TYPE + " TEXT," +
+ "_sync_id TEXT," +
+ "_sync_version TEXT," +
+ "_sync_time TEXT," + // UTC
+ "_sync_local_id INTEGER," +
+ "_sync_dirty INTEGER," +
+ "_sync_mark INTEGER," + // Used to filter out new rows
+ "url TEXT," +
+ "name TEXT," +
+ "displayName TEXT," +
+ "hidden INTEGER NOT NULL DEFAULT 0," +
+ "color INTEGER," +
+ "access_level INTEGER," +
+ "selected INTEGER NOT NULL DEFAULT 1," +
+ "sync_events INTEGER NOT NULL DEFAULT 0," +
+ "location TEXT," +
+ "timezone TEXT," +
+ "ownerAccount TEXT" +
+ ");");
+
+ // Trigger to remove a calendar's events when we delete the calendar
+ db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
+ "BEGIN " +
+ "DELETE FROM Events WHERE calendar_id = old._id;" +
+ "END");
+
+ // TODO: do we need both dtend and duration?
+ db.execSQL("CREATE TABLE Events (" +
+ "_id INTEGER PRIMARY KEY," +
+ ACCOUNT_NAME + " TEXT," +
+ ACCOUNT_TYPE + " TEXT," +
+ "_sync_id TEXT," +
+ "_sync_version TEXT," +
+ "_sync_time TEXT," + // UTC
+ "_sync_local_id INTEGER," +
+ "_sync_dirty INTEGER," +
+ "_sync_mark INTEGER," + // To filter out new rows
+ "calendar_id INTEGER NOT NULL," +
+ "htmlUri TEXT," +
+ "title TEXT," +
+ "eventLocation TEXT," +
+ "description TEXT," +
+ "eventStatus INTEGER," +
+ "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
+ "commentsUri TEXT," +
+ "dtstart INTEGER," + // millis since epoch
+ "dtend INTEGER," + // millis since epoch
+ "eventTimezone TEXT," + // timezone for event
+ "duration TEXT," +
+ "allDay INTEGER NOT NULL DEFAULT 0," +
+ "visibility INTEGER NOT NULL DEFAULT 0," +
+ "transparency INTEGER NOT NULL DEFAULT 0," +
+ "hasAlarm INTEGER NOT NULL DEFAULT 0," +
+ "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
+ "rrule TEXT," +
+ "rdate TEXT," +
+ "exrule TEXT," +
+ "exdate TEXT," +
+ "originalEvent TEXT," + // _sync_id of recurring event
+ "originalInstanceTime INTEGER," + // millis since epoch
+ "originalAllDay INTEGER," +
+ "lastDate INTEGER," + // millis since epoch
+ "hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
+ "guestsCanModify INTEGER NOT NULL DEFAULT 0," +
+ "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
+ "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
+ "organizer STRING," +
+ "deleted INTEGER NOT NULL DEFAULT 0" +
+ ");");
+
+ // Trigger to set event's sync_account
+ db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
+ "BEGIN " +
+ "UPDATE Events SET _sync_account=" +
+ "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," +
+ "_sync_account_type=" +
+ "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " +
+ "WHERE Events._id=new._id;" +
+ "END");
+
+ db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ + Calendar.Events._SYNC_ACCOUNT_TYPE + ", " + Calendar.Events._SYNC_ACCOUNT + ", "
+ + Calendar.Events._SYNC_ID + ");");
+
+ db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
+ Calendar.Events.CALENDAR_ID +
+ ");");
+
+ db.execSQL("CREATE TABLE EventsRawTimes (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER NOT NULL," +
+ "dtstart2445 TEXT," +
+ "dtend2445 TEXT," +
+ "originalInstanceTime2445 TEXT," +
+ "lastDate2445 TEXT," +
+ "UNIQUE (event_id)" +
+ ");");
+
+ db.execSQL("CREATE TABLE Instances (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER," +
+ "begin INTEGER," + // UTC millis
+ "end INTEGER," + // UTC millis
+ "startDay INTEGER," + // Julian start day
+ "endDay INTEGER," + // Julian end day
+ "startMinute INTEGER," + // minutes from midnight
+ "endMinute INTEGER," + // minutes from midnight
+ "UNIQUE (event_id, begin, end)" +
+ ");");
+
+ db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
+ Calendar.Instances.START_DAY +
+ ");");
+
+ db.execSQL("CREATE TABLE CalendarMetaData (" +
+ "_id INTEGER PRIMARY KEY," +
+ "localTimezone TEXT," +
+ "minInstance INTEGER," + // UTC millis
+ "maxInstance INTEGER" + // UTC millis
+ ");");
+
+ createCalendarCacheTable(db);
+
+ db.execSQL("CREATE TABLE Attendees (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER," +
+ "attendeeName TEXT," +
+ "attendeeEmail TEXT," +
+ "attendeeStatus INTEGER," +
+ "attendeeRelationship INTEGER," +
+ "attendeeType INTEGER" +
+ ");");
+
+ db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
+ Calendar.Attendees.EVENT_ID +
+ ");");
+
+ db.execSQL("CREATE TABLE Reminders (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER," +
+ "minutes INTEGER," +
+ "method INTEGER NOT NULL" +
+ " DEFAULT " + Calendar.Reminders.METHOD_DEFAULT +
+ ");");
+
+ db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
+ Calendar.Reminders.EVENT_ID +
+ ");");
+
+ // This table stores the Calendar notifications that have gone off.
+ db.execSQL("CREATE TABLE CalendarAlerts (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER," +
+ "begin INTEGER NOT NULL," + // UTC millis
+ "end INTEGER NOT NULL," + // UTC millis
+ "alarmTime INTEGER NOT NULL," + // UTC millis
+ "creationTime INTEGER NOT NULL," + // UTC millis
+ "receivedTime INTEGER NOT NULL," + // UTC millis
+ "notifyTime INTEGER NOT NULL," + // UTC millis
+ "state INTEGER NOT NULL," +
+ "minutes INTEGER," +
+ "UNIQUE (alarmTime, begin, event_id)" +
+ ");");
+
+ db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
+ Calendar.CalendarAlerts.EVENT_ID +
+ ");");
+
+ db.execSQL("CREATE TABLE ExtendedProperties (" +
+ "_id INTEGER PRIMARY KEY," +
+ "event_id INTEGER," +
+ "name TEXT," +
+ "value TEXT" +
+ ");");
+
+ db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
+ Calendar.ExtendedProperties.EVENT_ID +
+ ");");
+
+ // Trigger to remove data tied to an event when we delete that event.
+ db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
+ "BEGIN " +
+ "DELETE FROM Instances WHERE event_id = old._id;" +
+ "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
+ "DELETE FROM Attendees WHERE event_id = old._id;" +
+ "DELETE FROM Reminders WHERE event_id = old._id;" +
+ "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
+ "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
+ "END");
+
+ createEventsView(db);
+
+ ContentResolver.requestSync(null /* all accounts */,
+ ContactsContract.AUTHORITY, new Bundle());
+ }
+
+ private void createCalendarCacheTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE CalendarCache (" +
+ "_id INTEGER PRIMARY KEY," +
+ "key TEXT NOT NULL," +
+ "value TEXT" +
+ ");");
+
+ db.execSQL("INSERT INTO CalendarCache (key, value) VALUES (" +
+ "'" + CalendarCache.KEY_TIMEZONE_DATABASE_VERSION + "'," +
+ "'" + CalendarCache.DEFAULT_TIMEZONE_DATABASE_VERSION + "'" +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.i(TAG, "Upgrading DB from version " + oldVersion
+ + " to " + newVersion);
+ if (oldVersion < 49) {
+ dropTables(db);
+ mSyncState.createDatabase(db);
+ return; // this was lossy
+ }
+
+ if (oldVersion < 51) {
+ upgradeToVersion51(db); // From 50 or 51
+ oldVersion = 51;
+ }
+ if (oldVersion == 51) {
+ upgradeToVersion52(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 52) {
+ upgradeToVersion53(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 53) {
+ upgradeToVersion54(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 54) {
+ upgradeToVersion55(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 55 || oldVersion == 56) {
+ // Both require resync, so just schedule it once
+ upgradeResync(db);
+ }
+ if (oldVersion == 55) {
+ upgradeToVersion56(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 56) {
+ upgradeToVersion57(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 57) {
+ // Changes are undone upgrading to 60, so don't do anything.
+ oldVersion += 1;
+ }
+ if (oldVersion == 58) {
+ upgradeToVersion59(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 59) {
+ upgradeToVersion60(db);
+ oldVersion += 1;
+ }
+ if (oldVersion == 60) {
+ upgradeToVersion61(db);
+ oldVersion += 1;
+ }
+ }
+
+ private void upgradeToVersion56(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;");
+ // Clear _sync_dirty to avoid a client-to-server sync that could blow away
+ // server attendees.
+ // Clear _sync_version to pull down the server's event (with attendees)
+ // Change the URLs from full-selfattendance to full
+ db.execSQL("UPDATE Events"
+ + " SET _sync_dirty=0,"
+ + " _sync_version=NULL,"
+ + " _sync_id="
+ + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full'),"
+ + " commentsUri ="
+ + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');");
+ db.execSQL("UPDATE Calendars"
+ + " SET url="
+ + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
+
+ // "cursor" iterates over all the calendars
+ Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars",
+ null /* selection args */);
+ // Add the owner column.
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ Long id = cursor.getLong(0);
+ String url = cursor.getString(1);
+ String owner = calendarEmailAddressFromFeedUrl(url);
+ db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?",
+ new Object[] {owner, id});
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void upgradeResync(SQLiteDatabase db) {
+ // Delete sync state, so all records will be re-synced.
+ db.execSQL("DELETE FROM _sync_state;");
+
+ // "cursor" iterates over all the calendars
+ Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url "
+ + "FROM Calendars",
+ null /* selection args */);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ String accountName = cursor.getString(0);
+ String accountType = cursor.getString(1);
+ final Account account = new Account(accountName, accountType);
+ String calendarUrl = cursor.getString(2);
+ scheduleSync(account, false /* two-way sync */, calendarUrl);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void upgradeToVersion61(SQLiteDatabase db) {
+ createCalendarCacheTable(db);
+ }
+
+ private void upgradeToVersion60(SQLiteDatabase db) {
+ // Switch to CalendarProvider2
+ db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
+ db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
+ "BEGIN " +
+ "DELETE FROM Events WHERE calendar_id = old._id;" +
+ "END");
+ db.execSQL("ALTER TABLE Events ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;");
+ db.execSQL("DROP TRIGGER IF EXISTS events_insert");
+ db.execSQL("CREATE TRIGGER events_insert AFTER INSERT ON Events " +
+ "BEGIN " +
+ "UPDATE Events SET _sync_account=" +
+ "(SELECT _sync_account FROM Calendars WHERE Calendars._id=new.calendar_id)," +
+ "_sync_account_type=" +
+ "(SELECT _sync_account_type FROM Calendars WHERE Calendars._id=new.calendar_id) " +
+ "WHERE Events._id=new._id;" +
+ "END");
+ db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
+ db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
+ db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
+ "BEGIN " +
+ "DELETE FROM Instances WHERE event_id = old._id;" +
+ "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
+ "DELETE FROM Attendees WHERE event_id = old._id;" +
+ "DELETE FROM Reminders WHERE event_id = old._id;" +
+ "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
+ "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
+ "END");
+ db.execSQL("DROP TRIGGER IF EXISTS attendees_update");
+ db.execSQL("DROP TRIGGER IF EXISTS attendees_insert");
+ db.execSQL("DROP TRIGGER IF EXISTS attendees_delete");
+ db.execSQL("DROP TRIGGER IF EXISTS reminders_update");
+ db.execSQL("DROP TRIGGER IF EXISTS reminders_insert");
+ db.execSQL("DROP TRIGGER IF EXISTS reminders_delete");
+ db.execSQL("DROP TRIGGER IF EXISTS extended_properties_update");
+ db.execSQL("DROP TRIGGER IF EXISTS extended_properties_insert");
+ db.execSQL("DROP TRIGGER IF EXISTS extended_properties_delete");
+
+ createEventsView(db);
+ }
+
+ private void upgradeToVersion59(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS BusyBits;");
+ db.execSQL("CREATE TEMPORARY TABLE CalendarMetaData_Backup" +
+ "(_id,localTimezone,minInstance,maxInstance);");
+ db.execSQL("INSERT INTO CalendarMetaData_Backup " +
+ "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData;");
+ db.execSQL("DROP TABLE CalendarMetaData;");
+ db.execSQL("CREATE TABLE CalendarMetaData(_id,localTimezone,minInstance,maxInstance);");
+ db.execSQL("INSERT INTO CalendarMetaData " +
+ "SELECT _id,localTimezone,minInstance,maxInstance FROM CalendarMetaData_Backup;");
+ db.execSQL("DROP TABLE CalendarMetaData_Backup;");
+ }
+
+ private void upgradeToVersion57(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify"
+ + " INTEGER NOT NULL DEFAULT 0;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers"
+ + " INTEGER NOT NULL DEFAULT 1;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests"
+ + " INTEGER NOT NULL DEFAULT 1;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;");
+ db.execSQL("UPDATE Events SET organizer="
+ + "(SELECT attendeeEmail FROM Attendees WHERE "
+ + "Attendees.event_id = Events._id"
+ + " AND Attendees.attendeeRelationship=2);");
+ }
+
+ private void upgradeToVersion55(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
+ db.execSQL("UPDATE Calendars"
+ + " SET _sync_account_type='com.google'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE Events"
+ + " SET _sync_account_type='com.google'"
+ + " WHERE _sync_account IS NOT NULL");
+ db.execSQL("UPDATE DeletedEvents"
+ + " SET _sync_account_type='com.google'"
+ + " WHERE _sync_account IS NOT NULL");
+ Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
+ db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
+ db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ + Calendar.Events._SYNC_ACCOUNT_TYPE + ", "
+ + Calendar.Events._SYNC_ACCOUNT + ", "
+ + Calendar.Events._SYNC_ID + ");");
+ }
+
+ private void upgradeToVersion54(SQLiteDatabase db) {
+ Log.w(TAG, "adding eventSyncAccountAndIdIndex");
+ db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+ + Calendar.Events._SYNC_ACCOUNT + ", " + Calendar.Events._SYNC_ID + ");");
+ }
+
+ private void upgradeToVersion53(SQLiteDatabase db) {
+ Log.w(TAG, "Upgrading CalendarAlerts table");
+ db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
+ db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
+ db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
+ }
+
+ private void upgradeToVersion52(SQLiteDatabase db) {
+ // We added "originalAllDay" to the Events table to keep track of
+ // the allDay status of the original recurring event for entries
+ // that are exceptions to that recurring event. We need this so
+ // that we can format the date correctly for the "originalInstanceTime"
+ // column when we make a change to the recurrence exception and
+ // send it to the server.
+ db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");
+
+ // Iterate through the Events table and for each recurrence
+ // exception, fill in the correct value for "originalAllDay",
+ // if possible. The only times where this might not be possible
+ // are (1) the original recurring event no longer exists, or
+ // (2) the original recurring event does not yet have a _sync_id
+ // because it was created on the phone and hasn't been synced to the
+ // server yet. In both cases the originalAllDay field will be set
+ // to null. In the first case we don't care because the recurrence
+ // exception will not be displayed and we won't be able to make
+ // any changes to it (and even if we did, the server should ignore
+ // them, right?). In the second case, the calendar client already
+ // disallows making changes to an instance of a recurring event
+ // until the recurring event has been synced to the server so the
+ // second case should never occur.
+
+ // "cursor" iterates over all the recurrences exceptions.
+ Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
+ + " WHERE originalEvent IS NOT NULL", null /* selection args */);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String originalEvent = cursor.getString(1);
+
+ // Find the original recurring event (if it exists)
+ Cursor recur = db.rawQuery("SELECT allDay FROM Events"
+ + " WHERE _sync_id=?", new String[] {originalEvent});
+ if (recur == null) {
+ continue;
+ }
+
+ try {
+ // Fill in the "originalAllDay" field of the
+ // recurrence exception with the "allDay" value
+ // from the recurring event.
+ if (recur.moveToNext()) {
+ int allDay = recur.getInt(0);
+ db.execSQL("UPDATE Events SET originalAllDay=" + allDay
+ + " WHERE _id="+id);
+ }
+ } finally {
+ recur.close();
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void upgradeToVersion51(SQLiteDatabase db) {
+ Log.w(TAG, "Upgrading DeletedEvents table");
+
+ // We don't have enough information to fill in the correct
+ // value of the calendar_id for old rows in the DeletedEvents
+ // table, but rows in that table are transient so it is unlikely
+ // that there are any rows. Plus, the calendar_id is used only
+ // when deleting a calendar, which is a rare event. All new rows
+ // will have the correct calendar_id.
+ db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
+
+ // Trigger to remove a calendar's events when we delete the calendar
+ db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
+ db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
+ "BEGIN " +
+ "DELETE FROM Events WHERE calendar_id = old._id;" +
+ "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
+ "END");
+ db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
+ }
+
+ private void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS Calendars;");
+ db.execSQL("DROP TABLE IF EXISTS Events;");
+ db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
+ db.execSQL("DROP TABLE IF EXISTS Instances;");
+ db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
+ db.execSQL("DROP TABLE IF EXISTS CalendarCache;");
+ db.execSQL("DROP TABLE IF EXISTS Attendees;");
+ db.execSQL("DROP TABLE IF EXISTS Reminders;");
+ db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
+ db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
+ }
+
+ @Override
+ public synchronized SQLiteDatabase getWritableDatabase() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ return db;
+ }
+
+ public SyncStateContentProviderHelper getSyncState() {
+ return mSyncState;
+ }
+
+ /**
+ * Schedule a calendar sync for the account.
+ * @param account the account for which to schedule a sync
+ * @param uploadChangesOnly if set, specify that the sync should only send
+ * up local changes. This is typically used for a local sync, a user override of
+ * too many deletions, or a sync after a calendar is unselected.
+ * @param url the url feed for the calendar to sync (may be null, in which case a poll of
+ * all feeds is done.)
+ */
+ void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
+ Bundle extras = new Bundle();
+ if (uploadChangesOnly) {
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
+ }
+ if (url != null) {
+ extras.putString("feed", url);
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+ }
+ ContentResolver.requestSync(account, Calendar.Calendars.CONTENT_URI.getAuthority(), extras);
+ }
+
+ public void wipeData() {
+ SQLiteDatabase db = getWritableDatabase();
+
+ db.execSQL("DELETE FROM Calendars;");
+ db.execSQL("DELETE FROM Events;");
+ db.execSQL("DELETE FROM EventsRawTimes;");
+ db.execSQL("DELETE FROM Instances;");
+ db.execSQL("DELETE FROM CalendarMetaData;");
+ db.execSQL("DELETE FROM CalendarCache;");
+ db.execSQL("DELETE FROM Attendees;");
+ db.execSQL("DELETE FROM Reminders;");
+ db.execSQL("DELETE FROM CalendarAlerts;");
+ db.execSQL("DELETE FROM ExtendedProperties;");
+ }
+
+ public interface Views {
+ public static final String EVENTS = "view_events";
+ }
+
+ public interface Tables {
+ public static final String EVENTS = "Events";
+ public static final String CALENDARS = "Calendars";
+ }
+
+ private static void createEventsView(SQLiteDatabase db) {
+ db.execSQL("DROP VIEW IF EXISTS " + Views.EVENTS + ";");
+ String eventsSelect = "SELECT "
+ + Tables.EVENTS + "." + Calendar.Events._ID + " AS " + Calendar.Events._ID + ","
+ + Calendar.Events.HTML_URI + ","
+ + Calendar.Events.TITLE + ","
+ + Calendar.Events.DESCRIPTION + ","
+ + Calendar.Events.EVENT_LOCATION + ","
+ + Calendar.Events.STATUS + ","
+ + Calendar.Events.SELF_ATTENDEE_STATUS + ","
+ + Calendar.Events.COMMENTS_URI + ","
+ + Calendar.Events.DTSTART + ","
+ + Calendar.Events.DTEND + ","
+ + Calendar.Events.DURATION + ","
+ + Calendar.Events.EVENT_TIMEZONE + ","
+ + Calendar.Events.ALL_DAY + ","
+ + Calendar.Events.VISIBILITY + ","
+ + Calendar.Events.TIMEZONE + ","
+ + Calendar.Events.SELECTED + ","
+ + Calendar.Events.ACCESS_LEVEL + ","
+ + Calendar.Events.TRANSPARENCY + ","
+ + Calendar.Events.COLOR + ","
+ + Calendar.Events.HAS_ALARM + ","
+ + Calendar.Events.HAS_EXTENDED_PROPERTIES + ","
+ + Calendar.Events.RRULE + ","
+ + Calendar.Events.RDATE + ","
+ + Calendar.Events.EXRULE + ","
+ + Calendar.Events.EXDATE + ","
+ + Calendar.Events.ORIGINAL_EVENT + ","
+ + Calendar.Events.ORIGINAL_INSTANCE_TIME + ","
+ + Calendar.Events.ORIGINAL_ALL_DAY + ","
+ + Calendar.Events.LAST_DATE + ","
+ + Calendar.Events.HAS_ATTENDEE_DATA + ","
+ + Calendar.Events.CALENDAR_ID + ","
+ + Calendar.Events.GUESTS_CAN_INVITE_OTHERS + ","
+ + Calendar.Events.GUESTS_CAN_MODIFY + ","
+ + Calendar.Events.GUESTS_CAN_SEE_GUESTS + ","
+ + Calendar.Events.ORGANIZER + ","
+ + Calendar.Events.DELETED + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_ID
+ + " AS " + Calendar.Events._SYNC_ID + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_VERSION
+ + " AS " + Calendar.Events._SYNC_VERSION + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_DIRTY
+ + " AS " + Calendar.Events._SYNC_DIRTY + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT
+ + " AS " + Calendar.Events._SYNC_ACCOUNT + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_ACCOUNT_TYPE
+ + " AS " + Calendar.Events._SYNC_ACCOUNT_TYPE + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_TIME
+ + " AS " + Calendar.Events._SYNC_TIME + ","
+ + Tables.EVENTS + "." + Calendar.Events._SYNC_LOCAL_ID
+ + " AS " + Calendar.Events._SYNC_LOCAL_ID + ","
+ + Calendar.Calendars.URL + ","
+ + Calendar.Calendars.OWNER_ACCOUNT
+ + " FROM " + Tables.EVENTS + " JOIN " + Tables.CALENDARS
+ + " ON (" + Tables.EVENTS + "." + Calendar.Events.CALENDAR_ID
+ + "=" + Tables.CALENDARS + "." + Calendar.Calendars._ID
+ + ")";
+
+ db.execSQL("CREATE VIEW " + Views.EVENTS + " AS " + eventsSelect);
+ }
+
+ /**
+ * Extracts the calendar email from a calendar feed url.
+ * @param feed the calendar feed url
+ * @return the calendar email that is in the feed url or null if it can't
+ * find the email address.
+ * TODO: this is duplicated in CalendarSyncAdapter; move to a library
+ */
+ public static String calendarEmailAddressFromFeedUrl(String feed) {
+ // Example feed url:
+ // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
+ String[] pathComponents = feed.split("/");
+ if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
+ try {
+ return URLDecoder.decode(pathComponents[5], "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "unable to url decode the email address in calendar " + feed);
+ return null;
+ }
+ }
+
+ Log.e(TAG, "unable to find the email address in calendar " + feed);
+ return null;
+ }
+}
diff --git a/src/com/android/providers/calendar/CalendarDebugReceiver.java b/src/com/android/providers/calendar/CalendarDebugReceiver.java
index 2d468a9..c74bffa 100644
--- a/src/com/android/providers/calendar/CalendarDebugReceiver.java
+++ b/src/com/android/providers/calendar/CalendarDebugReceiver.java
@@ -16,11 +16,9 @@
package com.android.providers.calendar;
-import static android.provider.Telephony.Intents.SECRET_CODE_ACTION;
-
+import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.BroadcastReceiver;
public class CalendarDebugReceiver extends BroadcastReceiver {
@@ -28,13 +26,15 @@
public CalendarDebugReceiver() {
}
+ /**
+ * Receives android.provider.Telephony.Intents.SECRET_CODE_ACTION and
+ * displays debugging information.
+ */
@Override
public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(SECRET_CODE_ACTION)) {
- Intent i = new Intent(Intent.ACTION_MAIN);
- i.setClass(context, CalendarDebug.class);
- i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(i);
- }
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.setClass(context, CalendarDebug.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(i);
}
}
diff --git a/src/com/android/providers/calendar/CalendarProvider.java b/src/com/android/providers/calendar/CalendarProvider.java
deleted file mode 100644
index 4bc51dc..0000000
--- a/src/com/android/providers/calendar/CalendarProvider.java
+++ /dev/null
@@ -1,4372 +0,0 @@
-/*
-**
-** Copyright 2006, 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,
-** See the License for the specific language governing permissions and
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** limitations under the License.
-*/
-
-package com.android.providers.calendar;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.OperationCanceledException;
-import android.accounts.AuthenticatorException;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.content.AbstractSyncableContentProvider;
-import android.content.AbstractTableMerger;
-import android.content.BroadcastReceiver;
-import android.content.ContentProvider;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.content.EntityIterator;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.OperationApplicationException;
-import android.content.SyncContext;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Debug;
-import android.os.Process;
-import android.os.RemoteException;
-import android.pim.DateException;
-import android.pim.RecurrenceSet;
-import android.provider.Calendar;
-import android.provider.Calendar.Attendees;
-import android.provider.Calendar.BusyBits;
-import android.provider.Calendar.CalendarAlerts;
-import android.provider.Calendar.Calendars;
-import android.provider.Calendar.Events;
-import android.provider.Calendar.ExtendedProperties;
-import android.provider.Calendar.Instances;
-import android.provider.Calendar.Reminders;
-import android.text.TextUtils;
-import android.text.format.Time;
-import android.util.Config;
-import android.util.Log;
-import android.util.TimeFormatException;
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
-import com.google.android.googlelogin.GoogleLoginServiceConstants;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-import java.io.IOException;
-
-public class CalendarProvider extends AbstractSyncableContentProvider {
- private static final boolean PROFILE = false;
- private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
- private static final String[] ACCOUNTS_PROJECTION =
- new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE};
-
- private static final String[] EVENTS_PROJECTION = new String[] {
- Events._SYNC_ID,
- Events._SYNC_VERSION,
- Events._SYNC_ACCOUNT,
- Events._SYNC_ACCOUNT_TYPE,
- Events.CALENDAR_ID,
- Events.RRULE,
- Events.RDATE,
- Events.ORIGINAL_EVENT,
- };
- private static final int EVENTS_SYNC_ID_INDEX = 0;
- private static final int EVENTS_SYNC_VERSION_INDEX = 1;
- private static final int EVENTS_SYNC_ACCOUNT_NAME_INDEX = 2;
- private static final int EVENTS_SYNC_ACCOUNT_TYPE_INDEX = 3;
- private static final int EVENTS_CALENDAR_ID_INDEX = 4;
- private static final int EVENTS_RRULE_INDEX = 5;
- private static final int EVENTS_RDATE_INDEX = 6;
- private static final int EVENTS_ORIGINAL_EVENT_INDEX = 7;
-
- private DatabaseUtils.InsertHelper mCalendarsInserter;
- private DatabaseUtils.InsertHelper mEventsInserter;
- private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
- private DatabaseUtils.InsertHelper mDeletedEventsInserter;
- private DatabaseUtils.InsertHelper mInstancesInserter;
- private DatabaseUtils.InsertHelper mAttendeesInserter;
- private DatabaseUtils.InsertHelper mRemindersInserter;
- private DatabaseUtils.InsertHelper mCalendarAlertsInserter;
- private DatabaseUtils.InsertHelper mExtendedPropertiesInserter;
-
- /**
- * The cached copy of the CalendarMetaData database table.
- * Make this "package private" instead of "private" so that test code
- * can access it.
- */
- MetaData mMetaData;
-
- // The interval in minutes for calculating busy bits
- private static final int BUSYBIT_INTERVAL = 60;
-
- // A lookup table for getting a bit mask of length N, for N <= 32
- // For example, BIT_MASKS[4] gives 0xf (which has 4 bits set to 1).
- // We use this for computing the busy bits for events.
- private static final int[] BIT_MASKS = {
- 0,
- 0x00000001, 0x00000003, 0x00000007, 0x0000000f,
- 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff,
- 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff,
- 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff,
- 0x0001ffff, 0x0003ffff, 0x0007ffff, 0x000fffff,
- 0x001fffff, 0x003fffff, 0x007fffff, 0x00ffffff,
- 0x01ffffff, 0x03ffffff, 0x07ffffff, 0x0fffffff,
- 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff,
- };
-
- // To determine if a recurrence exception originally overlapped the
- // window, we need to assume a maximum duration, since we only know
- // the original start time.
- private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
-
- public static final class TimeRange {
- public long begin;
- public long end;
- public boolean allDay;
- }
-
- public static final class InstancesRange {
- public long begin;
- public long end;
-
- public InstancesRange(long begin, long end) {
- this.begin = begin;
- this.end = end;
- }
- }
-
- public static final class InstancesList
- extends ArrayList<ContentValues> {
- }
-
- public static final class EventInstancesMap
- extends HashMap<String, InstancesList> {
- public void add(String syncId, ContentValues values) {
- InstancesList instances = get(syncId);
- if (instances == null) {
- instances = new InstancesList();
- put(syncId, instances);
- }
- instances.add(values);
- }
- }
-
- // A thread that runs in the background and schedules the next
- // calendar event alarm.
- private class AlarmScheduler extends Thread {
- boolean mRemoveAlarms;
-
- public AlarmScheduler(boolean removeAlarms) {
- mRemoveAlarms = removeAlarms;
- }
-
- public void run() {
- try {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- runScheduleNextAlarm(mRemoveAlarms);
- } catch (SQLException e) {
- Log.e(TAG, "runScheduleNextAlarm() failed", e);
- }
- }
- }
-
- /**
- * We search backward in time for event reminders that we may have missed
- * and schedule them if the event has not yet expired. The amount in
- * the past to search backwards is controlled by this constant. It
- * should be at least a few minutes to allow for an event that was
- * recently created on the web to make its way to the phone. Two hours
- * might seem like overkill, but it is useful in the case where the user
- * just crossed into a new timezone and might have just missed an alarm.
- */
- private static final long SCHEDULE_ALARM_SLACK = 2 * android.text.format.DateUtils.HOUR_IN_MILLIS;
-
- /**
- * Alarms older than this threshold will be deleted from the CalendarAlerts
- * table. This should be at least a day because if the timezone is
- * wrong and the user corrects it we might delete good alarms that
- * appear to be old because the device time was incorrectly in the future.
- * This threshold must also be larger than SCHEDULE_ALARM_SLACK. We add
- * the SCHEDULE_ALARM_SLACK to ensure this.
- *
- * To make it easier to find and debug problems with missed reminders,
- * set this to something greater than a day.
- */
- private static final long CLEAR_OLD_ALARM_THRESHOLD =
- 7 * android.text.format.DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK;
-
- // A lock for synchronizing access to fields that are shared
- // with the AlarmScheduler thread.
- private Object mAlarmLock = new Object();
-
- private static final String TAG = "CalendarProvider";
- private static final String DATABASE_NAME = "calendar.db";
-
- // Note: if you update the version number, you must also update the code
- // in upgradeDatabase() to modify the database (gracefully, if possible).
- private static final int DATABASE_VERSION = 57;
-
- // Make sure we load at least two months worth of data.
- // Client apps can load more data in a background thread.
- private static final long MINIMUM_EXPANSION_SPAN =
- 2L * 31 * 24 * 60 * 60 * 1000;
-
- private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
- private static final int CALENDARS_INDEX_ID = 0;
-
- // Allocate the string constant once here instead of on the heap
- private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
-
- private static final String[] sInstancesProjection =
- new String[] { Instances.START_DAY, Instances.END_DAY,
- Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
-
- private static final int INSTANCES_INDEX_START_DAY = 0;
- private static final int INSTANCES_INDEX_END_DAY = 1;
- private static final int INSTANCES_INDEX_START_MINUTE = 2;
- private static final int INSTANCES_INDEX_END_MINUTE = 3;
- private static final int INSTANCES_INDEX_ALL_DAY = 4;
-
- private static final String[] sBusyBitProjection = new String[] {
- BusyBits.DAY, BusyBits.BUSYBITS, BusyBits.ALL_DAY_COUNT };
-
- private static final int BUSYBIT_INDEX_DAY = 0;
- private static final int BUSYBIT_INDEX_BUSYBITS= 1;
- private static final int BUSYBIT_INDEX_ALL_DAY_COUNT = 2;
-
- private CalendarClient mCalendarClient = null;
-
- private AlarmManager mAlarmManager;
-
- private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
-
- /**
- * Listens for timezone changes and disk-no-longer-full events
- */
- private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onReceive() " + action);
- }
- if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
- updateTimezoneDependentFields();
- scheduleNextAlarm(false /* do not remove alarms */);
- } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
- // Try to clean up if things were screwy due to a full disk
- updateTimezoneDependentFields();
- scheduleNextAlarm(false /* do not remove alarms */);
- } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
- scheduleNextAlarm(false /* do not remove alarms */);
- }
- }
- };
-
- public CalendarProvider() {
- super(DATABASE_NAME, DATABASE_VERSION, Calendars.CONTENT_URI);
- }
-
- @Override
- public boolean onCreate() {
- super.onCreate();
-
- setTempProviderSyncAdapter(new CalendarSyncAdapter(getContext(), this));
-
- // Register for Intent broadcasts
- IntentFilter filter = new IntentFilter();
-
- filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
- filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
- filter.addAction(Intent.ACTION_TIME_CHANGED);
- final Context c = getContext();
-
- // We don't ever unregister this because this thread always wants
- // to receive notifications, even in the background. And if this
- // thread is killed then the whole process will be killed and the
- // memory resources will be reclaimed.
- c.registerReceiver(mIntentReceiver, filter);
-
- mMetaData = new MetaData(mOpenHelper);
- updateTimezoneDependentFields();
-
- return true;
- }
-
- /**
- * This creates a background thread to check the timezone and update
- * the timezone dependent fields in the Instances table if the timezone
- * has changes.
- */
- private void updateTimezoneDependentFields() {
- Thread thread = new TimezoneCheckerThread();
- thread.start();
- }
-
- private class TimezoneCheckerThread extends Thread {
- @Override
- public void run() {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- try {
- doUpdateTimezoneDependentFields();
- } catch (SQLException e) {
- Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
- try {
- // Clear at least the in-memory data (and if possible the
- // database fields) to force a re-computation of Instances.
- mMetaData.clearInstanceRange();
- } catch (SQLException e2) {
- Log.e(TAG, "clearInstanceRange() also failed: " + e2);
- }
- }
- }
- }
-
- /**
- * This method runs in a background thread. If the timezone has changed
- * then the Instances table will be regenerated.
- */
- private void doUpdateTimezoneDependentFields() {
- MetaData.Fields fields = mMetaData.getFields();
- String localTimezone = TimeZone.getDefault().getID();
- if (TextUtils.equals(fields.timezone, localTimezone)) {
- // Even if the timezone hasn't changed, check for missed alarms.
- // This code executes when the CalendarProvider is created and
- // helps to catch missed alarms when the Calendar process is
- // killed (because of low-memory conditions) and then restarted.
- rescheduleMissedAlarms();
- return;
- }
-
- // The database timezone is different from the current timezone.
- // Regenerate the Instances table for this month. Include events
- // starting at the beginning of this month.
- long now = System.currentTimeMillis();
- Time time = new Time();
- time.set(now);
- time.monthDay = 1;
- time.hour = 0;
- time.minute = 0;
- time.second = 0;
- long begin = time.normalize(true);
- long end = begin + MINIMUM_EXPANSION_SPAN;
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- handleInstanceQuery(qb, begin, end, new String[] { Instances._ID },
- null /* selection */, null /* sort */, false /* searchByDayInsteadOfMillis */);
-
- // Also pre-compute the BusyBits table for this month.
- int startDay = Time.getJulianDay(begin, time.gmtoff);
- int endDay = startDay + 31;
- qb = new SQLiteQueryBuilder();
- handleBusyBitsQuery(qb, startDay, endDay, sBusyBitProjection,
- null /* selection */, null /* sort */);
- rescheduleMissedAlarms();
- }
-
- private void rescheduleMissedAlarms() {
- AlarmManager manager = getAlarmManager();
- if (manager != null) {
- Context context = getContext();
- ContentResolver cr = context.getContentResolver();
- CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
- }
- }
-
- @Override
- protected void onDatabaseOpened(SQLiteDatabase db) {
- db.markTableSyncable("Events", "DeletedEvents");
-
- if (!isTemporary()) {
- mCalendarClient = new CalendarClient(
- new AndroidGDataClient(getContext(), CalendarSyncAdapter.USER_AGENT_APP_VERSION),
- new XmlCalendarGDataParserFactory(
- new AndroidXmlParserFactory()));
- }
-
- mCalendarsInserter = new DatabaseUtils.InsertHelper(db, "Calendars");
- mEventsInserter = new DatabaseUtils.InsertHelper(db, "Events");
- mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, "EventsRawTimes");
- mDeletedEventsInserter = new DatabaseUtils.InsertHelper(db, "DeletedEvents");
- mInstancesInserter = new DatabaseUtils.InsertHelper(db, "Instances");
- mAttendeesInserter = new DatabaseUtils.InsertHelper(db, "Attendees");
- mRemindersInserter = new DatabaseUtils.InsertHelper(db, "Reminders");
- mCalendarAlertsInserter = new DatabaseUtils.InsertHelper(db, "CalendarAlerts");
- mExtendedPropertiesInserter =
- new DatabaseUtils.InsertHelper(db, "ExtendedProperties");
- }
-
- @Override
- protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.i(TAG, "Upgrading DB from version " + oldVersion
- + " to " + newVersion);
- if (oldVersion < 46) {
- dropTables(db);
- bootstrapDatabase(db);
- return false; // this was lossy
- }
-
- if (oldVersion == 46) {
- Log.w(TAG, "Upgrading CalendarAlerts table");
- db.execSQL("UPDATE CalendarAlerts SET reminder_id=NULL;");
- db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN minutes INTEGER DEFAULT 0;");
- oldVersion += 1;
- }
-
- if (oldVersion == 47) {
- // Changing to version 48 was intended to force a data wipe
- dropTables(db);
- bootstrapDatabase(db);
- return false; // this was lossy
- }
-
- if (oldVersion == 48) {
- // Changing to version 49 was intended to force a data wipe
- dropTables(db);
- bootstrapDatabase(db);
- return false; // this was lossy
- }
-
- if (oldVersion == 49) {
- Log.w(TAG, "Upgrading DeletedEvents table");
-
- // We don't have enough information to fill in the correct
- // value of the calendar_id for old rows in the DeletedEvents
- // table, but rows in that table are transient so it is unlikely
- // that there are any rows. Plus, the calendar_id is used only
- // when deleting a calendar, which is a rare event. All new rows
- // will have the correct calendar_id.
- db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN calendar_id INTEGER;");
-
- // Trigger to remove a calendar's events when we delete the calendar
- db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
- db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
- "BEGIN " +
- "DELETE FROM Events WHERE calendar_id = old._id;" +
- "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
- "END");
- db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
- oldVersion += 1;
- }
-
- if (oldVersion == 50) {
- // This should have been deleted in the upgrade from version 49
- // but we missed it.
- db.execSQL("DROP TRIGGER IF EXISTS event_to_deleted");
- oldVersion += 1;
- }
-
- if (oldVersion == 51) {
- // We added "originalAllDay" to the Events table to keep track of
- // the allDay status of the original recurring event for entries
- // that are exceptions to that recurring event. We need this so
- // that we can format the date correctly for the "originalInstanceTime"
- // column when we make a change to the recurrence exception and
- // send it to the server.
- db.execSQL("ALTER TABLE Events ADD COLUMN originalAllDay INTEGER;");
-
- // Iterate through the Events table and for each recurrence
- // exception, fill in the correct value for "originalAllDay",
- // if possible. The only times where this might not be possible
- // are (1) the original recurring event no longer exists, or
- // (2) the original recurring event does not yet have a _sync_id
- // because it was created on the phone and hasn't been synced to the
- // server yet. In both cases the originalAllDay field will be set
- // to null. In the first case we don't care because the recurrence
- // exception will not be displayed and we won't be able to make
- // any changes to it (and even if we did, the server should ignore
- // them, right?). In the second case, the calendar client already
- // disallows making changes to an instance of a recurring event
- // until the recurring event has been synced to the server so the
- // second case should never occur.
-
- // "cursor" iterates over all the recurrences exceptions.
- Cursor cursor = db.rawQuery("SELECT _id,originalEvent FROM Events"
- + " WHERE originalEvent IS NOT NULL", null /* selection args */);
- if (cursor != null) {
- try {
- while (cursor.moveToNext()) {
- long id = cursor.getLong(0);
- String originalEvent = cursor.getString(1);
-
- // Find the original recurring event (if it exists)
- Cursor recur = db.rawQuery("SELECT allDay FROM Events"
- + " WHERE _sync_id=?", new String[] {originalEvent});
- if (recur == null) {
- continue;
- }
-
- try {
- // Fill in the "originalAllDay" field of the
- // recurrence exception with the "allDay" value
- // from the recurring event.
- if (recur.moveToNext()) {
- int allDay = recur.getInt(0);
- db.execSQL("UPDATE Events SET originalAllDay=" + allDay
- + " WHERE _id="+id);
- }
- } finally {
- recur.close();
- }
- }
- } finally {
- cursor.close();
- }
- }
- oldVersion += 1;
- }
-
- if (oldVersion == 52) {
- Log.w(TAG, "Upgrading CalendarAlerts table");
- db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN creationTime INTEGER DEFAULT 0;");
- db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN receivedTime INTEGER DEFAULT 0;");
- db.execSQL("ALTER TABLE CalendarAlerts ADD COLUMN notifyTime INTEGER DEFAULT 0;");
- oldVersion += 1;
- }
-
- if (oldVersion == 53) {
- Log.w(TAG, "adding eventSyncAccountAndIdIndex");
- db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
- + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");
- oldVersion += 1;
- }
-
- if (oldVersion == 54) {
- db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
- db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
- db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
- db.execSQL("UPDATE Calendars"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE Events"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE DeletedEvents"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
- db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
- db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
- + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
- + Events._SYNC_ID + ");");
- oldVersion += 1;
- }
- if (oldVersion == 55 || oldVersion == 56) { // Both require resync
- // Delete sync state, so all records will be re-synced.
- db.execSQL("DELETE FROM _sync_state;");
-
- // "cursor" iterates over all the calendars
- Cursor cursor = db.rawQuery("SELECT _sync_account,_sync_account_type,url "
- + "FROM Calendars",
- null /* selection args */);
- if (cursor != null) {
- try {
- while (cursor.moveToNext()) {
- String accountName = cursor.getString(0);
- String accountType = cursor.getString(1);
- final Account account = new Account(accountName, accountType);
- String calendarUrl = cursor.getString(2);
- scheduleSync(account, false /* two-way sync */, calendarUrl);
- }
- } finally {
- cursor.close();
- }
- }
- }
- if (oldVersion == 55) {
- db.execSQL("ALTER TABLE Calendars ADD COLUMN ownerAccount TEXT;");
- db.execSQL("ALTER TABLE Events ADD COLUMN hasAttendeeData INTEGER;");
- // Clear _sync_dirty to avoid a client-to-server sync that could blow away
- // server attendees.
- // Clear _sync_version to pull down the server's event (with attendees)
- // Change the URLs from full-selfattendance to full
- db.execSQL("UPDATE Events"
- + " SET _sync_dirty=0,"
- + " _sync_version=NULL,"
- + " _sync_id="
- + "REPLACE(_sync_id, '/private/full-selfattendance', '/private/full'),"
- + " commentsUri ="
- + "REPLACE(commentsUri, '/private/full-selfattendance', '/private/full');");
- db.execSQL("UPDATE Calendars"
- + " SET url="
- + "REPLACE(url, '/private/full-selfattendance', '/private/full');");
-
- // "cursor" iterates over all the calendars
- Cursor cursor = db.rawQuery("SELECT _id, url FROM Calendars",
- null /* selection args */);
- // Add the owner column.
- if (cursor != null) {
- try {
- while (cursor.moveToNext()) {
- Long id = cursor.getLong(0);
- String url = cursor.getString(1);
- String owner = CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(url);
- db.execSQL("UPDATE Calendars SET ownerAccount=? WHERE _id=?",
- new Object[] {owner, id});
- }
- } finally {
- cursor.close();
- }
- }
- oldVersion += 1;
- }
- if (oldVersion == 56) {
- db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanModify"
- + " INTEGER NOT NULL DEFAULT 0;");
- db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanInviteOthers"
- + " INTEGER NOT NULL DEFAULT 1;");
- db.execSQL("ALTER TABLE Events ADD COLUMN guestsCanSeeGuests"
- + " INTEGER NOT NULL DEFAULT 1;");
- db.execSQL("ALTER TABLE Events ADD COLUMN organizer STRING;");
- db.execSQL("UPDATE Events SET organizer="
- + "(SELECT attendeeEmail FROM Attendees WHERE "
- + "Attendees.event_id = Events._id AND Attendees.attendeeRelationship=2);");
-
-
- oldVersion += 1;
- }
-
- return true; // this was lossless
- }
-
- private void dropTables(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS Calendars;");
- db.execSQL("DROP TABLE IF EXISTS Events;");
- db.execSQL("DROP TABLE IF EXISTS EventsRawTimes;");
- db.execSQL("DROP TABLE IF EXISTS DeletedEvents;");
- db.execSQL("DROP TABLE IF EXISTS Instances;");
- db.execSQL("DROP TABLE IF EXISTS CalendarMetaData;");
- db.execSQL("DROP TABLE IF EXISTS BusyBits;");
- db.execSQL("DROP TABLE IF EXISTS Attendees;");
- db.execSQL("DROP TABLE IF EXISTS Reminders;");
- db.execSQL("DROP TABLE IF EXISTS CalendarAlerts;");
- db.execSQL("DROP TABLE IF EXISTS ExtendedProperties;");
- }
-
- @Override
- protected void bootstrapDatabase(SQLiteDatabase db) {
- super.bootstrapDatabase(db);
- db.execSQL("CREATE TABLE Calendars (" +
- "_id INTEGER PRIMARY KEY," +
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- "_sync_id TEXT," +
- "_sync_version TEXT," +
- "_sync_time TEXT," + // UTC
- "_sync_local_id INTEGER," +
- "_sync_dirty INTEGER," +
- "_sync_mark INTEGER," + // Used to filter out new rows
- "url TEXT," +
- "name TEXT," +
- "displayName TEXT," +
- "hidden INTEGER NOT NULL DEFAULT 0," +
- "color INTEGER," +
- "access_level INTEGER," +
- "selected INTEGER NOT NULL DEFAULT 1," +
- "sync_events INTEGER NOT NULL DEFAULT 0," +
- "location TEXT," +
- "timezone TEXT," +
- "ownerAccount TEXT" +
- ");");
-
- // Trigger to remove a calendar's events when we delete the calendar
- db.execSQL("CREATE TRIGGER calendar_cleanup DELETE ON Calendars " +
- "BEGIN " +
- "DELETE FROM Events WHERE calendar_id = old._id;" +
- "DELETE FROM DeletedEvents WHERE calendar_id = old._id;" +
- "END");
-
- // TODO: do we need both dtend and duration?
- db.execSQL("CREATE TABLE Events (" +
- "_id INTEGER PRIMARY KEY," +
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- "_sync_id TEXT," +
- "_sync_version TEXT," +
- "_sync_time TEXT," + // UTC
- "_sync_local_id INTEGER," +
- "_sync_dirty INTEGER," +
- "_sync_mark INTEGER," + // To filter out new rows
- "calendar_id INTEGER NOT NULL," +
- "htmlUri TEXT," +
- "title TEXT," +
- "eventLocation TEXT," +
- "description TEXT," +
- "eventStatus INTEGER," +
- "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0," +
- "commentsUri TEXT," +
- "dtstart INTEGER," + // millis since epoch
- "dtend INTEGER," + // millis since epoch
- "eventTimezone TEXT," + // timezone for event
- "duration TEXT," +
- "allDay INTEGER NOT NULL DEFAULT 0," +
- "visibility INTEGER NOT NULL DEFAULT 0," +
- "transparency INTEGER NOT NULL DEFAULT 0," +
- "hasAlarm INTEGER NOT NULL DEFAULT 0," +
- "hasExtendedProperties INTEGER NOT NULL DEFAULT 0," +
- "rrule TEXT," +
- "rdate TEXT," +
- "exrule TEXT," +
- "exdate TEXT," +
- "originalEvent TEXT," + // _sync_id of recurring event
- "originalInstanceTime INTEGER," + // millis since epoch
- "originalAllDay INTEGER," +
- "lastDate INTEGER," + // millis since epoch
- "hasAttendeeData INTEGER NOT NULL DEFAULT 0," +
- "guestsCanModify INTEGER NOT NULL DEFAULT 0," +
- "guestsCanInviteOthers INTEGER NOT NULL DEFAULT 1," +
- "guestsCanSeeGuests INTEGER NOT NULL DEFAULT 1," +
- "organizer STRING" +
- ");");
-
- db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
- + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
- + Events._SYNC_ID + ");");
-
- db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
- Events.CALENDAR_ID +
- ");");
-
- db.execSQL("CREATE TABLE EventsRawTimes (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER NOT NULL," +
- "dtstart2445 TEXT," +
- "dtend2445 TEXT," +
- "originalInstanceTime2445 TEXT," +
- "lastDate2445 TEXT," +
- "UNIQUE (event_id)" +
- ");");
-
- // NOTE: we do not create a trigger to delete an event's instances upon update,
- // as all rows currently get updated during a merge.
-
- db.execSQL("CREATE TABLE DeletedEvents (" +
- "_sync_id TEXT," +
- "_sync_version TEXT," +
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
- "_sync_mark INTEGER," + // To filter out new rows
- "calendar_id INTEGER" +
- ");");
-
- db.execSQL("CREATE TABLE Instances (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER," +
- "begin INTEGER," + // UTC millis
- "end INTEGER," + // UTC millis
- "startDay INTEGER," + // Julian start day
- "endDay INTEGER," + // Julian end day
- "startMinute INTEGER," + // minutes from midnight
- "endMinute INTEGER," + // minutes from midnight
- "UNIQUE (event_id, begin, end)" +
- ");");
-
- db.execSQL("CREATE INDEX instancesStartDayIndex ON Instances (" +
- Instances.START_DAY +
- ");");
-
- db.execSQL("CREATE TABLE CalendarMetaData (" +
- "_id INTEGER PRIMARY KEY," +
- "localTimezone TEXT," +
- "minInstance INTEGER," + // UTC millis
- "maxInstance INTEGER," + // UTC millis
- "minBusyBits INTEGER," + // UTC millis
- "maxBusyBits INTEGER" + // UTC millis
- ");");
-
- db.execSQL("CREATE TABLE BusyBits(" +
- "day INTEGER PRIMARY KEY," + // the Julian day
- "busyBits INTEGER," + // 24 bits for 60-minute intervals
- "allDayCount INTEGER" + // number of all-day events
- ");");
-
- db.execSQL("CREATE TABLE Attendees (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER," +
- "attendeeName TEXT," +
- "attendeeEmail TEXT," +
- "attendeeStatus INTEGER," +
- "attendeeRelationship INTEGER," +
- "attendeeType INTEGER" +
- ");");
-
- db.execSQL("CREATE INDEX attendeesEventIdIndex ON Attendees (" +
- Attendees.EVENT_ID +
- ");");
-
- db.execSQL("CREATE TABLE Reminders (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER," +
- "minutes INTEGER," +
- "method INTEGER NOT NULL" +
- " DEFAULT " + Reminders.METHOD_DEFAULT +
- ");");
-
- db.execSQL("CREATE INDEX remindersEventIdIndex ON Reminders (" +
- Reminders.EVENT_ID +
- ");");
-
- // This table stores the Calendar notifications that have gone off.
- db.execSQL("CREATE TABLE CalendarAlerts (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER," +
- "begin INTEGER NOT NULL," + // UTC millis
- "end INTEGER NOT NULL," + // UTC millis
- "alarmTime INTEGER NOT NULL," + // UTC millis
- "creationTime INTEGER NOT NULL," + // UTC millis
- "receivedTime INTEGER NOT NULL," + // UTC millis
- "notifyTime INTEGER NOT NULL," + // UTC millis
- "state INTEGER NOT NULL," +
- "minutes INTEGER," +
- "UNIQUE (alarmTime, begin, event_id)" +
- ");");
-
- db.execSQL("CREATE INDEX calendarAlertsEventIdIndex ON CalendarAlerts (" +
- CalendarAlerts.EVENT_ID +
- ");");
-
- db.execSQL("CREATE TABLE ExtendedProperties (" +
- "_id INTEGER PRIMARY KEY," +
- "event_id INTEGER," +
- "name TEXT," +
- "value TEXT" +
- ");");
-
- db.execSQL("CREATE INDEX extendedPropertiesEventIdIndex ON ExtendedProperties (" +
- ExtendedProperties.EVENT_ID +
- ");");
-
- // Trigger to remove data tied to an event when we delete that event.
- db.execSQL("CREATE TRIGGER events_cleanup_delete DELETE ON Events " +
- "BEGIN " +
- "DELETE FROM Instances WHERE event_id = old._id;" +
- "DELETE FROM EventsRawTimes WHERE event_id = old._id;" +
- "DELETE FROM Attendees WHERE event_id = old._id;" +
- "DELETE FROM Reminders WHERE event_id = old._id;" +
- "DELETE FROM CalendarAlerts WHERE event_id = old._id;" +
- "DELETE FROM ExtendedProperties WHERE event_id = old._id;" +
- "END");
-
- // Triggers to set the _sync_dirty flag when an attendee is changed,
- // inserted or deleted
- db.execSQL("CREATE TRIGGER attendees_update UPDATE ON Attendees " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER attendees_insert INSERT ON Attendees " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER attendees_delete DELETE ON Attendees " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
-
- // Triggers to set the _sync_dirty flag when a reminder is changed,
- // inserted or deleted
- db.execSQL("CREATE TRIGGER reminders_update UPDATE ON Reminders " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER reminders_insert INSERT ON Reminders " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER reminders_delete DELETE ON Reminders " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
- // Triggers to set the _sync_dirty flag when an extended property is changed,
- // inserted or deleted
- db.execSQL("CREATE TRIGGER extended_properties_update UPDATE ON ExtendedProperties " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER extended_properties_insert UPDATE ON ExtendedProperties " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=new.event_id;" +
- "END");
- db.execSQL("CREATE TRIGGER extended_properties_delete UPDATE ON ExtendedProperties " +
- "BEGIN " +
- "UPDATE Events SET _sync_dirty=1 WHERE Events._id=old.event_id;" +
- "END");
- }
-
- /**
- * Make sure that there are no entries for accounts that no longer
- * exist. We are overriding this since we need to delete from the
- * Calendars table, which is not syncable, which has triggers that
- * will delete from the Events and DeletedEvents tables, which are
- * syncable.
- */
- @Override
- protected void onAccountsChanged(final Account[] accountsArray) {
- super.onAccountsChanged(accountsArray);
-
- final Map<Account, Boolean> accounts = Maps.newHashMap();
- for (Account account : accountsArray) {
- accounts.put(account, false);
- }
-
- mDb.beginTransaction();
- try {
- deleteRowsForRemovedAccounts(accounts, "Calendars");
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
-
- if (mCalendarClient == null) {
- return;
- }
-
- // If we have calendars for unknown accounts, delete them.
- // If there are no calendars at all for a given account, add the
- // default calendar.
-
- // TODO: allow caller to specify which account's feeds should be updated
- String[] features = new String[]{
- GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE};
- AccountManagerCallback<Account[]> callback = new AccountManagerCallback<Account[]>() {
- public void run(AccountManagerFuture<Account[]> accountManagerFuture) {
- Account[] currentAccounts = new Account[0];
- try {
- currentAccounts = accountManagerFuture.getResult();
- } catch (OperationCanceledException e) {
- Log.w(TAG, "onAccountsChanged", e);
- return;
- } catch (IOException e) {
- Log.w(TAG, "onAccountsChanged", e);
- return;
- } catch (AuthenticatorException e) {
- Log.w(TAG, "onAccountsChanged", e);
- return;
- }
- if (currentAccounts.length < 1) {
- Log.w(TAG, "getPrimaryAccount: no primary account configured.");
- return;
- }
- Account primaryAccount = currentAccounts[0];
-
- for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
- // TODO: change this when Calendar supports multiple accounts. Until then
- // pretend that only the primary exists.
- boolean ignore = primaryAccount == null ||
- !primaryAccount.equals(entry.getKey());
- entry.setValue(ignore);
- }
-
- Set<Account> handledAccounts = Sets.newHashSet();
- if (Config.LOGV) Log.v(TAG, "querying calendars");
- Cursor c = queryInternal(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null,
- null);
- try {
- while (c.moveToNext()) {
- final String accountName = c.getString(0);
- final String accountType = c.getString(1);
- final Account account = new Account(accountName, accountType);
- if (handledAccounts.contains(account)) {
- continue;
- }
- handledAccounts.add(account);
- if (accounts.containsKey(account)) {
- if (Config.LOGV) {
- Log.v(TAG, "calendars for account " + account + " exist");
- }
- accounts.put(account, true /* hasCalendar */);
- }
- }
- } finally {
- c.close();
- c = null;
- }
-
- if (Config.LOGV) {
- Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
- }
- for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
- final Account account = entry.getKey();
- boolean hasCalendar = entry.getValue();
- if (hasCalendar) {
- if (Config.LOGV) {
- Log.v(TAG, "ignoring account " + account +
- " since it matched an existing calendar");
- }
- continue;
- }
- String feedUrl = mCalendarClient.getDefaultCalendarUrl(account.name,
- CalendarClient.PROJECTION_PRIVATE_FULL, null/* query params */);
- feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
- if (Config.LOGV) {
- Log.v(TAG, "adding default calendar for account " + account);
- }
- ContentValues values = new ContentValues();
- values.put(Calendars._SYNC_ACCOUNT, account.name);
- values.put(Calendars._SYNC_ACCOUNT_TYPE, account.type);
- values.put(Calendars.URL, feedUrl);
- values.put(Calendars.OWNER_ACCOUNT,
- CalendarSyncAdapter.calendarEmailAddressFromFeedUrl(feedUrl));
- values.put(Calendars.DISPLAY_NAME,
- getContext().getString(R.string.calendar_default_name));
- values.put(Calendars.SYNC_EVENTS, 1);
- values.put(Calendars.SELECTED, 1);
- values.put(Calendars.HIDDEN, 0);
- values.put(Calendars.COLOR, -14069085 /* blue */);
- // this is just our best guess. the real value will get updated
- // when the user does a sync.
- values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
- values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
- insertInternal(Calendars.CONTENT_URI, values);
-
- scheduleSync(account, false /* do a full sync */, null /* no url */);
-
- }
- // Call the CalendarSyncAdapter's onAccountsChanged
- getTempProviderSyncAdapter().onAccountsChanged(accountsArray);
- }
- };
-
- AccountManager.get(getContext()).getAccountsByTypeAndFeatures(
- GoogleLoginServiceConstants.ACCOUNT_TYPE, features, callback, null);
- }
-
- @Override
- public Cursor queryInternal(Uri url, String[] projectionIn,
- String selection, String[] selectionArgs, String sort) {
- final SQLiteDatabase db = getDatabase();
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
- Cursor ret;
-
- // Generate the body of the query
- int match = sURLMatcher.match(url);
- switch (match)
- {
- case EVENTS:
- qb.setTables("Events, Calendars");
- qb.setProjectionMap(sEventsProjectionMap);
- qb.appendWhere("Events.calendar_id=Calendars._id");
- break;
- case EVENTS_ID:
- qb.setTables("Events, Calendars");
- qb.setProjectionMap(sEventsProjectionMap);
- qb.appendWhere("Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case DELETED_EVENTS:
- if (isTemporary()) {
- qb.setTables("DeletedEvents");
- break;
- } else {
- throw new IllegalArgumentException("Unknown URL " + url);
- }
- case CALENDARS:
- qb.setTables("Calendars");
- break;
- case CALENDARS_ID:
- qb.setTables("Calendars");
- qb.appendWhere("_id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case INSTANCES:
- case INSTANCES_BY_DAY:
- long begin;
- long end;
- try {
- begin = Long.valueOf(url.getPathSegments().get(2));
- } catch (NumberFormatException nfe) {
- throw new IllegalArgumentException("Cannot parse begin "
- + url.getPathSegments().get(2));
- }
- try {
- end = Long.valueOf(url.getPathSegments().get(3));
- } catch (NumberFormatException nfe) {
- throw new IllegalArgumentException("Cannot parse end "
- + url.getPathSegments().get(3));
- }
- return handleInstanceQuery(qb, begin, end, projectionIn,
- selection, sort, match == INSTANCES_BY_DAY);
- case BUSYBITS:
- int startDay;
- int endDay;
- try {
- startDay = Integer.valueOf(url.getPathSegments().get(2));
- } catch (NumberFormatException nfe) {
- throw new IllegalArgumentException("Cannot parse start day "
- + url.getPathSegments().get(2));
- }
- try {
- endDay = Integer.valueOf(url.getPathSegments().get(3));
- } catch (NumberFormatException nfe) {
- throw new IllegalArgumentException("Cannot parse end day "
- + url.getPathSegments().get(3));
- }
- return handleBusyBitsQuery(qb, startDay, endDay, projectionIn,
- selection, sort);
- case ATTENDEES:
- qb.setTables("Attendees, Events, Calendars");
- qb.setProjectionMap(sAttendeesProjectionMap);
- qb.appendWhere("Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=Attendees.event_id");
- break;
- case ATTENDEES_ID:
- qb.setTables("Attendees, Events, Calendars");
- qb.setProjectionMap(sAttendeesProjectionMap);
- qb.appendWhere("Attendees._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- qb.appendWhere(" AND Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=Attendees.event_id");
- break;
- case REMINDERS:
- qb.setTables("Reminders");
- break;
- case REMINDERS_ID:
- qb.setTables("Reminders, Events, Calendars");
- qb.setProjectionMap(sRemindersProjectionMap);
- qb.appendWhere("Reminders._id=");
- qb.appendWhere(url.getLastPathSegment());
- qb.appendWhere(" AND Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=Reminders.event_id");
- break;
- case CALENDAR_ALERTS:
- qb.setTables("CalendarAlerts, Events, Calendars");
- qb.setProjectionMap(sCalendarAlertsProjectionMap);
- qb.appendWhere("Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
- break;
- case CALENDAR_ALERTS_BY_INSTANCE:
- qb.setTables("CalendarAlerts, Events, Calendars");
- qb.setProjectionMap(sCalendarAlertsProjectionMap);
- qb.appendWhere("Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
- String groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
- return qb.query(db, projectionIn, selection, selectionArgs,
- groupBy, null, sort);
- case CALENDAR_ALERTS_ID:
- qb.setTables("CalendarAlerts, Events, Calendars");
- qb.setProjectionMap(sCalendarAlertsProjectionMap);
- qb.appendWhere("CalendarAlerts._id=");
- qb.appendWhere(url.getLastPathSegment());
- qb.appendWhere(" AND Events.calendar_id=Calendars._id");
- qb.appendWhere(" AND Events._id=CalendarAlerts.event_id");
- break;
- case EXTENDED_PROPERTIES:
- qb.setTables("ExtendedProperties");
- break;
- case EXTENDED_PROPERTIES_ID:
- qb.setTables("ExtendedProperties, Events, Calendars");
- // not sure if we need a projection map or a join. see what callers want.
-// qb.setProjectionMap(sExtendedPropertiesProjectionMap);
- qb.appendWhere("ExtendedProperties._id=");
- qb.appendWhere(url.getPathSegments().get(1));
-// qb.appendWhere(" AND Events.calendar_id = Calendars._id");
-// qb.appendWhere(" AND Events._id=ExtendedProperties.event_id");
- break;
-
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
-
- // run the query
- ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
-
- return ret;
- }
-
- /*
- * Fills the Instances table, if necessary, for the given range and then
- * queries the Instances table.
- *
- * @param qb The query
- * @param rangeBegin start of range (Julian days or ms)
- * @param rangeEnd end of range (Julian days or ms)
- * @param projectionIn The projection
- * @param selection The selection
- * @param sort How to sort
- * @param searchByDay if true, range is in Julian days, if false, range is in ms
- * @return
- */
- private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
- long rangeEnd, String[] projectionIn,
- String selection, String sort, boolean searchByDay) {
- final SQLiteDatabase db = getDatabase();
-
- qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
- "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
- qb.setProjectionMap(sInstancesProjectionMap);
- if (searchByDay) {
- // Convert the first and last Julian day range to a range that uses
- // UTC milliseconds.
- Time time = new Time();
- long beginMs = time.setJulianDay((int) rangeBegin);
- // We add one to lastDay because the time is set to 12am on the given
- // Julian day and we want to include all the events on the last day.
- long endMs = time.setJulianDay((int) rangeEnd + 1);
- // will lock the database.
- acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */);
- qb.appendWhere("startDay <= ");
- qb.appendWhere(String.valueOf(rangeEnd));
- qb.appendWhere(" AND endDay >= ");
- } else {
- // will lock the database.
- acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */);
- qb.appendWhere("begin <= ");
- qb.appendWhere(String.valueOf(rangeEnd));
- qb.appendWhere(" AND end >= ");
- }
- qb.appendWhere(String.valueOf(rangeBegin));
- return qb.query(db, projectionIn, selection, null, null, null, sort);
- }
-
- private Cursor handleBusyBitsQuery(SQLiteQueryBuilder qb, int startDay,
- int endDay, String[] projectionIn,
- String selection, String sort) {
- final SQLiteDatabase db = getDatabase();
- acquireBusyBitRange(startDay, endDay);
- qb.setTables("BusyBits");
- qb.setProjectionMap(sBusyBitsProjectionMap);
- qb.appendWhere("day >= ");
- qb.appendWhere(String.valueOf(startDay));
- qb.appendWhere(" AND day <= ");
- qb.appendWhere(String.valueOf(endDay));
- return qb.query(db, projectionIn, selection, null, null, null, sort);
- }
-
- /**
- * Ensure that the date range given has all elements in the instance
- * table. Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
- *
- * @param begin start of range (ms)
- * @param end end of range (ms)
- * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
- */
- private void acquireInstanceRange(final long begin,
- final long end,
- final boolean useMinimumExpansionWindow) {
- mDb.beginTransaction();
- try {
- acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow);
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
-
- /**
- * Expands the Instances table (if needed) and the BusyBits table.
- * Acquires the database lock and calls {@link #acquireBusyBitRangeLocked}.
- */
- private void acquireBusyBitRange(final int startDay, final int endDay) {
- mDb.beginTransaction();
- try {
- acquireBusyBitRangeLocked(startDay, endDay);
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
-
- /**
- * Ensure that the date range given has all elements in the instance
- * table. The database lock must be held when calling this method.
- *
- * @param begin start of range (ms)
- * @param end end of range (ms)
- * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
- */
- private void acquireInstanceRangeLocked(long begin, long end,
- boolean useMinimumExpansionWindow) {
- long expandBegin = begin;
- long expandEnd = end;
-
- if (useMinimumExpansionWindow) {
- // if we end up having to expand events into the instances table, expand
- // events for a minimal amount of time, so we do not have to perform
- // expansions frequently.
- long span = end - begin;
- if (span < MINIMUM_EXPANSION_SPAN) {
- long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
- expandBegin -= additionalRange;
- expandEnd += additionalRange;
- }
- }
-
- // Check if the timezone has changed.
- // We do this check here because the database is locked and we can
- // safely delete all the entries in the Instances table.
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- String dbTimezone = fields.timezone;
- long maxInstance = fields.maxInstance;
- long minInstance = fields.minInstance;
- String localTimezone = TimeZone.getDefault().getID();
- boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
-
- if (maxInstance == 0 || timezoneChanged) {
- // Empty the Instances table and expand from scratch.
- mDb.execSQL("DELETE FROM Instances;");
- mDb.execSQL("DELETE FROM BusyBits;");
- if (Config.LOGV) {
- Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances and Busybits,"
- + " timezone changed: " + timezoneChanged);
- }
- expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone);
-
- mMetaData.writeLocked(localTimezone, expandBegin, expandEnd,
- 0 /* startDay */, 0 /* endDay */);
- return;
- }
-
- // If the desired range [begin, end] has already been
- // expanded, then simply return. The range is inclusive, that is,
- // events that touch either endpoint are included in the expansion.
- // This means that a zero-duration event that starts and ends at
- // the endpoint will be included.
- // We use [begin, end] here and not [expandBegin, expandEnd] for
- // checking the range because a common case is for the client to
- // request successive days or weeks, for example. If we checked
- // that the expanded range [expandBegin, expandEnd] then we would
- // always be expanding because there would always be one more day
- // or week that hasn't been expanded.
- if ((begin >= minInstance) && (end <= maxInstance)) {
- if (Config.LOGV) {
- Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
- + ") falls within previously expanded range.");
- }
- return;
- }
-
- // If the requested begin point has not been expanded, then include
- // more events than requested in the expansion (use "expandBegin").
- if (begin < minInstance) {
- expandInstanceRangeLocked(expandBegin, minInstance, localTimezone);
- minInstance = expandBegin;
- }
-
- // If the requested end point has not been expanded, then include
- // more events than requested in the expansion (use "expandEnd").
- if (end > maxInstance) {
- expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone);
- maxInstance = expandEnd;
- }
-
- // Update the bounds on the Instances table.
- mMetaData.writeLocked(localTimezone, minInstance, maxInstance,
- fields.minBusyBit, fields.maxBusyBit);
- }
-
- private void acquireBusyBitRangeLocked(int firstDay, int lastDay) {
- if (firstDay > lastDay) {
- throw new IllegalArgumentException("firstDay must not be greater than lastDay");
- }
- String localTimezone = TimeZone.getDefault().getID();
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- String dbTimezone = fields.timezone;
- int minBusyBit = fields.minBusyBit;
- int maxBusyBit = fields.maxBusyBit;
- boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
- if (firstDay >= minBusyBit && lastDay <= maxBusyBit && !timezoneChanged) {
- if (Config.LOGV) {
- Log.v(TAG, "acquireBusyBitRangeLocked() no expansion needed");
- }
- return;
- }
-
- // Avoid gaps in the BusyBit table and avoid recomputing the busy bits
- // that are already in the table. If the busy bit range has been cleared,
- // don't bother checking.
- if (maxBusyBit != 0) {
- if (firstDay > maxBusyBit) {
- firstDay = maxBusyBit;
- } else if (lastDay < minBusyBit) {
- lastDay = minBusyBit;
- } else if (firstDay < minBusyBit && lastDay <= maxBusyBit) {
- lastDay = minBusyBit;
- } else if (lastDay > maxBusyBit && firstDay >= minBusyBit) {
- firstDay = maxBusyBit;
- }
- }
-
- // Allocate space for the busy bits, one 32-bit integer for each day.
- int numDays = lastDay - firstDay + 1;
- int[] busybits = new int[numDays];
- int[] allDayCounts = new int[numDays];
-
- // Convert the first and last Julian day range to a range that uses
- // UTC milliseconds.
- Time time = new Time();
- long begin = time.setJulianDay(firstDay);
-
- // We add one to lastDay because the time is set to 12am on the given
- // Julian day and we want to include all the events on the last day.
- long end = time.setJulianDay(lastDay + 1);
-
- // Make sure the Instances table includes events in the range
- // [begin, end].
- acquireInstanceRange(begin, end, true /* use minimum expansion window */);
-
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
- "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
- qb.setProjectionMap(sInstancesProjectionMap);
- qb.appendWhere("begin <= ");
- qb.appendWhere(String.valueOf(end));
- qb.appendWhere(" AND end >= ");
- qb.appendWhere(String.valueOf(begin));
- qb.appendWhere(" AND ");
- qb.appendWhere(Instances.SELECTED);
- qb.appendWhere("=1");
-
- final SQLiteDatabase db = getDatabase();
- // Get all the instances that overlap the range [begin,end]
- Cursor cursor = qb.query(db, sInstancesProjection, null, null, null, null, null);
- int count = 0;
- try {
- count = cursor.getCount();
- while (cursor.moveToNext()) {
- int startDay = cursor.getInt(INSTANCES_INDEX_START_DAY);
- int endDay = cursor.getInt(INSTANCES_INDEX_END_DAY);
- int startMinute = cursor.getInt(INSTANCES_INDEX_START_MINUTE);
- int endMinute = cursor.getInt(INSTANCES_INDEX_END_MINUTE);
- boolean allDay = cursor.getInt(INSTANCES_INDEX_ALL_DAY) != 0;
- fillBusyBits(firstDay, startDay, endDay, startMinute, endMinute,
- allDay, busybits, allDayCounts);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- if (count == 0) {
- return;
- }
-
- // Read the busybit range again because that may have changed when we
- // called acquireInstanceRange().
- fields = mMetaData.getFieldsLocked();
- minBusyBit = fields.minBusyBit;
- maxBusyBit = fields.maxBusyBit;
-
- // If the busybit range was cleared, then delete all the entries.
- if (maxBusyBit == 0) {
- mDb.execSQL("DELETE FROM BusyBits;");
- }
-
- // Merge the busy bits with the database.
- mergeBusyBits(firstDay, lastDay, busybits, allDayCounts);
- if (maxBusyBit == 0) {
- minBusyBit = firstDay;
- maxBusyBit = lastDay;
- } else {
- if (firstDay < minBusyBit) {
- minBusyBit = firstDay;
- }
- if (lastDay > maxBusyBit) {
- maxBusyBit = lastDay;
- }
- }
- // Update the busy bit range
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- minBusyBit, maxBusyBit);
- }
-
- private static final String[] EXPAND_COLUMNS = new String[] {
- Events._ID,
- Events._SYNC_ID,
- Events.STATUS,
- Events.DTSTART,
- Events.DTEND,
- Events.EVENT_TIMEZONE,
- Events.RRULE,
- Events.RDATE,
- Events.EXRULE,
- Events.EXDATE,
- Events.DURATION,
- Events.ALL_DAY,
- Events.ORIGINAL_EVENT,
- Events.ORIGINAL_INSTANCE_TIME
- };
-
- /**
- * Make instances for the given range.
- */
- private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
-
- if (PROFILE) {
- Debug.startMethodTracing("expandInstanceRangeLocked");
- }
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Expanding events between " + begin + " and " + end);
- }
-
- Cursor entries = getEntries(begin, end);
- try {
- performInstanceExpansion(begin, end, localTimezone, entries);
- } finally {
- if (entries != null) {
- entries.close();
- }
- }
- if (PROFILE) {
- Debug.stopMethodTracing();
- }
- }
-
- /**
- * Get all entries affecting the given window.
- * @param begin Window start (ms).
- * @param end Window end (ms).
- * @return Cursor for the entries; caller must close it.
- */
- private Cursor getEntries(long begin, long end) {
- final SQLiteDatabase db = getDatabase();
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
- qb.setProjectionMap(sEventsProjectionMap);
-
- String beginString = String.valueOf(begin);
- String endString = String.valueOf(end);
-
- qb.appendWhere("(dtstart <= ");
- qb.appendWhere(endString);
- qb.appendWhere(" AND ");
- qb.appendWhere("(lastDate IS NULL OR lastDate >= ");
- qb.appendWhere(beginString);
- qb.appendWhere(")) OR (");
- // grab recurrence exceptions that fall outside our expansion window but modify
- // recurrences that do fall within our window. we won't insert these into the output
- // set of instances, but instead will just add them to our cancellations list, so we
- // can cancel the correct recurrence expansion instances.
- qb.appendWhere("originalInstanceTime IS NOT NULL ");
- qb.appendWhere("AND originalInstanceTime <= ");
- qb.appendWhere(endString);
- qb.appendWhere(" AND ");
- // we don't have originalInstanceDuration or end time. for now, assume the original
- // instance lasts no longer than 1 week.
- // TODO: compute the originalInstanceEndTime or get this from the server.
- qb.appendWhere("originalInstanceTime >= ");
- qb.appendWhere(String.valueOf(begin - MAX_ASSUMED_DURATION));
- qb.appendWhere(")");
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Retrieving events to expand: " + qb.toString());
- }
-
- return qb.query(db, EXPAND_COLUMNS, null, null, null, null, null);
- }
-
- /**
- * Perform instance expansion on the given entries.
- * @param begin Window start (ms).
- * @param end Window end (ms).
- * @param localTimezone
- * @param entries The entries to process.
- */
- private void performInstanceExpansion(long begin, long end, String localTimezone, Cursor entries) {
- RecurrenceProcessor rp = new RecurrenceProcessor();
-
- int statusColumn = entries.getColumnIndex(Events.STATUS);
- int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
- int dtendColumn = entries.getColumnIndex(Events.DTEND);
- int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
- int durationColumn = entries.getColumnIndex(Events.DURATION);
- int rruleColumn = entries.getColumnIndex(Events.RRULE);
- int rdateColumn = entries.getColumnIndex(Events.RDATE);
- int exruleColumn = entries.getColumnIndex(Events.EXRULE);
- int exdateColumn = entries.getColumnIndex(Events.EXDATE);
- int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
- int idColumn = entries.getColumnIndex(Events._ID);
- int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
- int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
- int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
-
- ContentValues initialValues;
- EventInstancesMap instancesMap = new EventInstancesMap();
-
- Duration duration = new Duration();
- Time eventTime = new Time();
-
- // Invariant: entries contains all events that affect the current
- // window. It consists of:
- // a) Individual events that fall in the window. These will be
- // displayed.
- // b) Recurrences that included the window. These will be displayed
- // if not canceled.
- // c) Recurrence exceptions that fall in the window. These will be
- // displayed if not cancellations.
- // d) Recurrence exceptions that modify an instance inside the
- // window (subject to 1 week assumption above), but are outside
- // the window. These will not be displayed. Cases c and d are
- // distingushed by the start / end time.
-
- while (entries.moveToNext()) {
- try {
- initialValues = null;
-
- boolean allDay = entries.getInt(allDayColumn) != 0;
-
- String eventTimezone = entries.getString(eventTimezoneColumn);
- if (allDay || TextUtils.isEmpty(eventTimezone)) {
- // in the events table, allDay events start at midnight.
- // this forces them to stay at midnight for all day events
- // TODO: check that this actually does the right thing.
- eventTimezone = Time.TIMEZONE_UTC;
- }
-
- long dtstartMillis = entries.getLong(dtstartColumn);
- Long eventId = Long.valueOf(entries.getLong(idColumn));
-
- String durationStr = entries.getString(durationColumn);
- if (durationStr != null) {
- try {
- duration.parse(durationStr);
- }
- catch (DateException e) {
- Log.w(TAG, "error parsing duration for event "
- + eventId + "'" + durationStr + "'", e);
- duration.sign = 1;
- duration.weeks = 0;
- duration.days = 0;
- duration.hours = 0;
- duration.minutes = 0;
- duration.seconds = 0;
- durationStr = "+P0S";
- }
- }
-
- String syncId = entries.getString(syncIdColumn);
- String originalEvent = entries.getString(originalEventColumn);
-
- long originalInstanceTimeMillis = -1;
- if (!entries.isNull(originalInstanceTimeColumn)) {
- originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
- }
- int status = entries.getInt(statusColumn);
-
- String rruleStr = entries.getString(rruleColumn);
- String rdateStr = entries.getString(rdateColumn);
- String exruleStr = entries.getString(exruleColumn);
- String exdateStr = entries.getString(exdateColumn);
-
- RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
-
- if (recur.hasRecurrence()) {
- // the event is repeating
-
- if (status == Events.STATUS_CANCELED) {
- // should not happen!
- Log.e(TAG, "Found canceled recurring event in "
- + "Events table. Ignoring.");
- continue;
- }
-
- // need to parse the event into a local calendar.
- eventTime.timezone = eventTimezone;
- eventTime.set(dtstartMillis);
- eventTime.allDay = allDay;
-
- if (durationStr == null) {
- // should not happen.
- Log.e(TAG, "Repeating event has no duration -- "
- + "should not happen.");
- if (allDay) {
- // set to one day.
- duration.sign = 1;
- duration.weeks = 0;
- duration.days = 1;
- duration.hours = 0;
- duration.minutes = 0;
- duration.seconds = 0;
- durationStr = "+P1D";
- } else {
- // compute the duration from dtend, if we can.
- // otherwise, use 0s.
- duration.sign = 1;
- duration.weeks = 0;
- duration.days = 0;
- duration.hours = 0;
- duration.minutes = 0;
- if (!entries.isNull(dtendColumn)) {
- long dtendMillis = entries.getLong(dtendColumn);
- duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
- durationStr = "+P" + duration.seconds + "S";
- } else {
- duration.seconds = 0;
- durationStr = "+P0S";
- }
- }
- }
-
- long[] dates;
- dates = rp.expand(eventTime, recur, begin, end);
-
- // Initialize the "eventTime" timezone outside the loop.
- // This is used in computeTimezoneDependentFields().
- if (allDay) {
- eventTime.timezone = Time.TIMEZONE_UTC;
- } else {
- eventTime.timezone = localTimezone;
- }
-
- long durationMillis = duration.getMillis();
- for (long date : dates) {
- initialValues = new ContentValues();
- initialValues.put(Instances.EVENT_ID, eventId);
-
- initialValues.put(Instances.BEGIN, date);
- long dtendMillis = date + durationMillis;
- initialValues.put(Instances.END, dtendMillis);
-
- computeTimezoneDependentFields(date, dtendMillis,
- eventTime, initialValues);
- instancesMap.add(syncId, initialValues);
- }
- } else {
- // the event is not repeating
- initialValues = new ContentValues();
-
- // if this event has an "original" field, then record
- // that we need to cancel the original event (we can't
- // do that here because the order of this loop isn't
- // defined)
- if (originalEvent != null && originalInstanceTimeMillis != -1) {
- initialValues.put(Events.ORIGINAL_EVENT, originalEvent);
- initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
- originalInstanceTimeMillis);
- initialValues.put(Events.STATUS, status);
- }
-
- long dtendMillis = dtstartMillis;
- if (durationStr == null) {
- if (!entries.isNull(dtendColumn)) {
- dtendMillis = entries.getLong(dtendColumn);
- }
- } else {
- dtendMillis = duration.addTo(dtstartMillis);
- }
-
- // this non-recurring event might be a recurrence exception that doesn't
- // actually fall within our expansion window, but instead was selected
- // so we can correctly cancel expanded recurrence instances below. do not
- // add events to the instances map if they don't actually fall within our
- // expansion window.
- if ((dtendMillis < begin) || (dtstartMillis > end)) {
- if (originalEvent != null && originalInstanceTimeMillis != -1) {
- initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
- } else {
- Log.w(TAG, "Unexpected event outside window: " + syncId);
- continue;
- }
- }
-
- initialValues.put(Instances.EVENT_ID, eventId);
- initialValues.put(Instances.BEGIN, dtstartMillis);
-
- initialValues.put(Instances.END, dtendMillis);
-
- if (allDay) {
- eventTime.timezone = Time.TIMEZONE_UTC;
- } else {
- eventTime.timezone = localTimezone;
- }
- computeTimezoneDependentFields(dtstartMillis, dtendMillis,
- eventTime, initialValues);
-
- instancesMap.add(syncId, initialValues);
- }
- } catch (DateException e) {
- Log.w(TAG, "RecurrenceProcessor error ", e);
- } catch (TimeFormatException e) {
- Log.w(TAG, "RecurrenceProcessor error ", e);
- }
- }
-
- // Invariant: instancesMap contains all instances that affect the
- // window, indexed by original sync id. It consists of:
- // a) Individual events that fall in the window. They have:
- // EVENT_ID, BEGIN, END
- // b) Instances of recurrences that fall in the window. They may
- // be subject to exceptions. They have:
- // EVENT_ID, BEGIN, END
- // c) Exceptions that fall in the window. They have:
- // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS (since they can
- // be a modification or cancellation), EVENT_ID, BEGIN, END
- // d) Recurrence exceptions that modify an instance inside the
- // window but fall outside the window. They have:
- // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS =
- // STATUS_CANCELED, EVENT_ID, BEGIN, END
-
- // First, delete the original instances corresponding to recurrence
- // exceptions. We do this by iterating over the list and for each
- // recurrence exception, we search the list for an instance with a
- // matching "original instance time". If we find such an instance,
- // we remove it from the list. If we don't find such an instance
- // then we cancel the recurrence exception.
- Set<String> keys = instancesMap.keySet();
- for (String syncId : keys) {
- InstancesList list = instancesMap.get(syncId);
- for (ContentValues values : list) {
-
- // If this instance is not a recurrence exception, then
- // skip it.
- if (!values.containsKey(Events.ORIGINAL_EVENT)) {
- continue;
- }
-
- String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
- long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- InstancesList originalList = instancesMap.get(originalEvent);
- if (originalList == null) {
- // The original recurrence is not present, so don't try canceling it.
- continue;
- }
-
- // Search the original event for a matching original
- // instance time. If there is a matching one, then remove
- // the original one. We do this both for exceptions that
- // change the original instance as well as for exceptions
- // that delete the original instance.
- for (int num = originalList.size() - 1; num >= 0; num--) {
- ContentValues originalValues = originalList.get(num);
- long beginTime = originalValues.getAsLong(Instances.BEGIN);
- if (beginTime == originalTime) {
- // We found the original instance, so remove it.
- originalList.remove(num);
- }
- }
- }
- }
-
- // Invariant: instancesMap contains filtered instances.
- // It consists of:
- // a) Individual events that fall in the window.
- // b) Instances of recurrences that fall in the window and have not
- // been subject to exceptions.
- // c) Exceptions that fall in the window. They will have
- // STATUS_CANCELED if they are cancellations.
- // d) Recurrence exceptions that modify an instance inside the
- // window but fall outside the window. These are STATUS_CANCELED.
-
- // Now do the inserts. Since the db lock is held when this method is executed,
- // this will be done in a transaction.
- // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
- // while the calendar app is trying to query the db (expanding instances)), we will
- // not be "polite" and yield the lock until we're done. This will favor local query
- // operations over sync/write operations.
- for (String syncId : keys) {
- InstancesList list = instancesMap.get(syncId);
- for (ContentValues values : list) {
-
- // If this instance was cancelled then don't create a new
- // instance.
- Integer status = values.getAsInteger(Events.STATUS);
- if (status != null && status == Events.STATUS_CANCELED) {
- continue;
- }
-
- // Remove these fields before inserting a new instance
- values.remove(Events.ORIGINAL_EVENT);
- values.remove(Events.ORIGINAL_INSTANCE_TIME);
- values.remove(Events.STATUS);
-
- mInstancesInserter.replace(values);
- }
- }
- }
-
- /**
- * Computes the timezone-dependent fields of an instance of an event and
- * updates the "values" map to contain those fields.
- *
- * @param begin the start time of the instance (in UTC milliseconds)
- * @param end the end time of the instance (in UTC milliseconds)
- * @param local a Time object with the timezone set to the local timezone
- * @param values a map that will contain the timezone-dependent fields
- */
- private void computeTimezoneDependentFields(long begin, long end,
- Time local, ContentValues values) {
- local.set(begin);
- int startDay = Time.getJulianDay(begin, local.gmtoff);
- int startMinute = local.hour * 60 + local.minute;
-
- local.set(end);
- int endDay = Time.getJulianDay(end, local.gmtoff);
- int endMinute = local.hour * 60 + local.minute;
-
- // Special case for midnight, which has endMinute == 0. Change
- // that to +24 hours on the previous day to make everything simpler.
- // Exception: if start and end minute are both 0 on the same day,
- // then leave endMinute alone.
- if (endMinute == 0 && endDay > startDay) {
- endMinute = 24 * 60;
- endDay -= 1;
- }
-
- values.put(Instances.START_DAY, startDay);
- values.put(Instances.END_DAY, endDay);
- values.put(Instances.START_MINUTE, startMinute);
- values.put(Instances.END_MINUTE, endMinute);
- }
-
- private void fillBusyBits(int minDay, int startDay, int endDay, int startMinute,
- int endMinute, boolean allDay, int[] busybits, int[] allDayCounts) {
-
- // The startDay can be less than the minDay if we have an event
- // that starts earlier than the time range we are interested in.
- // In that case, we ignore the time range that falls outside the
- // the range we are interested in.
- if (startDay < minDay) {
- startDay = minDay;
- startMinute = 0;
- }
-
- // Likewise, truncate the event's end day so that it doesn't go past
- // the expected range.
- int numDays = busybits.length;
- int stopDay = endDay;
- if (stopDay > minDay + numDays - 1) {
- stopDay = minDay + numDays - 1;
- }
- int dayIndex = startDay - minDay;
-
- if (allDay) {
- for (int day = startDay; day <= stopDay; day++, dayIndex++) {
- allDayCounts[dayIndex] += 1;
- }
- return;
- }
-
- for (int day = startDay; day <= stopDay; day++, dayIndex++) {
- int endTime = endMinute;
- // If the event ends on a future day, then show it extending to
- // the end of this day.
- if (endDay > day) {
- endTime = 24 * 60;
- }
-
- int startBit = startMinute / BUSYBIT_INTERVAL ;
- int endBit = (endTime + BUSYBIT_INTERVAL - 1) / BUSYBIT_INTERVAL;
- int len = endBit - startBit;
- if (len == 0) {
- len = 1;
- }
- if (len < 0 || len > 24) {
- Log.e("Cal", "fillBusyBits() error: len " + len
- + " startMinute,endTime " + startMinute + " , " + endTime
- + " startDay,endDay " + startDay + " , " + endDay);
- } else {
- int oneBits = BIT_MASKS[len];
- busybits[dayIndex] |= oneBits << startBit;
- }
-
- // Set the start minute to the beginning of the day, in
- // case this event spans multiple days.
- startMinute = 0;
- }
- }
-
- private void mergeBusyBits(int startDay, int endDay, int[] busybits, int[] allDayCounts) {
- mDb.beginTransaction();
- try {
- mergeBusyBitsLocked(startDay, endDay, busybits, allDayCounts);
- mDb.setTransactionSuccessful();
- } finally {
- mDb.endTransaction();
- }
- }
-
- private void mergeBusyBitsLocked(int startDay, int endDay, int[] busybits,
- int[] allDayCounts) {
- final SQLiteDatabase db = getDatabase();
- Cursor cursor = null;
- try {
- String selection = "day>=" + startDay + " AND day<=" + endDay;
- cursor = db.query("BusyBits", sBusyBitProjection, selection, null, null, null, null);
- if (cursor == null) {
- return;
- }
- while (cursor.moveToNext()) {
- int day = cursor.getInt(BUSYBIT_INDEX_DAY);
- int busy = cursor.getInt(BUSYBIT_INDEX_BUSYBITS);
- int allDayCount = cursor.getInt(BUSYBIT_INDEX_ALL_DAY_COUNT);
-
- int dayIndex = day - startDay;
- busybits[dayIndex] |= busy;
- allDayCounts[dayIndex] += allDayCount;
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- // Allocate a map that we can reuse
- ContentValues values = new ContentValues();
-
- // Write the busy bits to the database
- int len = busybits.length;
- for (int dayIndex = 0; dayIndex < len; dayIndex++) {
- int busy = busybits[dayIndex];
- int allDayCount = allDayCounts[dayIndex];
- if (busy == 0 && allDayCount == 0) {
- continue;
- }
- int day = startDay + dayIndex;
-
- values.clear();
- values.put(BusyBits.DAY, day);
- values.put(BusyBits.BUSYBITS, busy);
- values.put(BusyBits.ALL_DAY_COUNT, allDayCount);
- db.replace("BusyBits", null, values);
- }
- }
-
- /**
- * Updates the BusyBit table when a new event is inserted into the Events
- * table. This is called after the event has been entered into the Events
- * table. If the event time is not within the date range of the current
- * BusyBits table, then the busy bits are not updated. The BusyBits
- * table is not automatically expanded to include this event.
- *
- * @param eventId the id of the newly created event
- * @param values the ContentValues for the new event
- */
- private void insertBusyBitsLocked(long eventId, ContentValues values) {
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- if (fields.maxBusyBit == 0) {
- return;
- }
-
- // If this is a recurrence event, then the expanded Instances range
- // should be 0 because this is called after updateInstancesLocked().
- // But for now check this condition and report an error if it occurs.
- // In the future, we could even support recurring events by
- // expanding them here and updating the busy bits for each instance.
- if (isRecurrenceEvent(values)) {
- Log.e(TAG, "insertBusyBitsLocked(): unexpected recurrence event\n");
- return;
- }
-
- long dtstartMillis = values.getAsLong(Events.DTSTART);
- Long dtendMillis = values.getAsLong(Events.DTEND);
- if (dtendMillis == null) {
- dtendMillis = dtstartMillis;
- }
-
- boolean allDay = false;
- Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
- if (allDayInteger != null) {
- allDay = allDayInteger != 0;
- }
-
- Time time = new Time();
- if (allDay) {
- time.timezone = Time.TIMEZONE_UTC;
- }
-
- ContentValues busyValues = new ContentValues();
- computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
-
- int startDay = busyValues.getAsInteger(Instances.START_DAY);
- int endDay = busyValues.getAsInteger(Instances.END_DAY);
-
- // If the event time is not in the expanded BusyBits range,
- // then return.
- if (startDay > fields.maxBusyBit || endDay < fields.minBusyBit) {
- return;
- }
-
- // Allocate space for the busy bits, one 32-bit integer for each day,
- // plus 24 bytes for the count of events that occur in each time slot.
- int numDays = endDay - startDay + 1;
- int[] busybits = new int[numDays];
- int[] allDayCounts = new int[numDays];
-
- int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
- int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
- fillBusyBits(startDay, startDay, endDay, startMinute, endMinute,
- allDay, busybits, allDayCounts);
- mergeBusyBits(startDay, endDay, busybits, allDayCounts);
- }
-
- /**
- * Updates the busy bits for an event that is being updated. This is
- * called before the event is updated in the Events table because we need
- * to know the time of the event before it was changed.
- *
- * @param eventId the id of the event being updated
- * @param values the ContentValues for the updated event
- */
- private void updateBusyBitsLocked(long eventId, ContentValues values) {
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- if (fields.maxBusyBit == 0) {
- return;
- }
-
- // If this is a recurring event, then clear the BusyBits table.
- if (isRecurrenceEvent(values)) {
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
- return;
- }
-
- // If the event fields being updated don't contain the start or end
- // time, then we don't need to bother updating the BusyBits table.
- Long dtstartLong = values.getAsLong(Events.DTSTART);
- Long dtendLong = values.getAsLong(Events.DTEND);
- if (dtstartLong == null && dtendLong == null) {
- return;
- }
-
- // If the timezone has changed, then clear the busy bits table
- // and return.
- String dbTimezone = fields.timezone;
- String localTimezone = TimeZone.getDefault().getID();
- boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
- if (timezoneChanged) {
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
- return;
- }
-
- // Read the existing event start and end times from the Events table.
- TimeRange eventRange = readEventStartEnd(eventId);
-
- // Fill in the new start time (if missing) or the new end time (if
- // missing) from the existing event start and end times.
- long dtstartMillis;
- if (dtstartLong != null) {
- dtstartMillis = dtstartLong;
- } else {
- dtstartMillis = eventRange.begin;
- }
-
- long dtendMillis;
- if (dtendLong != null) {
- dtendMillis = dtendLong;
- } else {
- dtendMillis = eventRange.end;
- }
-
- // Compute the start and end Julian days for the event.
- Time time = new Time();
- if (eventRange.allDay) {
- time.timezone = Time.TIMEZONE_UTC;
- }
- ContentValues busyValues = new ContentValues();
- computeTimezoneDependentFields(eventRange.begin, eventRange.end, time, busyValues);
- int oldStartDay = busyValues.getAsInteger(Instances.START_DAY);
- int oldEndDay = busyValues.getAsInteger(Instances.END_DAY);
-
- boolean allDay = false;
- Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
- if (allDayInteger != null) {
- allDay = allDayInteger != 0;
- }
-
- if (allDay) {
- time.timezone = Time.TIMEZONE_UTC;
- } else {
- time.timezone = TimeZone.getDefault().getID();
- }
-
- computeTimezoneDependentFields(dtstartMillis, dtendMillis, time, busyValues);
- int newStartDay = busyValues.getAsInteger(Instances.START_DAY);
- int newEndDay = busyValues.getAsInteger(Instances.END_DAY);
-
- // If both the old and new event times are outside the expanded
- // BusyBits table, then return.
- if ((oldStartDay > fields.maxBusyBit || oldEndDay < fields.minBusyBit)
- && (newStartDay > fields.maxBusyBit || newEndDay < fields.minBusyBit)) {
- return;
- }
-
- // If the old event time is within the expanded Instances range,
- // then clear the BusyBits table and return.
- if (oldStartDay <= fields.maxBusyBit && oldEndDay >= fields.minBusyBit) {
- // We could recompute the busy bits for the days containing the
- // old event time. For now, just clear the BusyBits table.
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
- return;
- }
-
- // The new event time is within the expanded Instances range.
- // So insert the busy bits for that day (or days).
-
- // Allocate space for the busy bits, one 32-bit integer for each day,
- // plus 24 bytes for the count of events that occur in each time slot.
- int numDays = newEndDay - newStartDay + 1;
- int[] busybits = new int[numDays];
- int[] allDayCounts = new int[numDays];
-
- int startMinute = busyValues.getAsInteger(Instances.START_MINUTE);
- int endMinute = busyValues.getAsInteger(Instances.END_MINUTE);
- fillBusyBits(newStartDay, newStartDay, newEndDay, startMinute, endMinute,
- allDay, busybits, allDayCounts);
- mergeBusyBits(newStartDay, newEndDay, busybits, allDayCounts);
- }
-
- /**
- * This method is called just before an event is deleted.
- *
- * @param eventId
- */
- private void deleteBusyBitsLocked(long eventId) {
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- if (fields.maxBusyBit == 0) {
- return;
- }
-
- // TODO: if the event being deleted is not a recurring event and the
- // start and end time are outside the BusyBit range, then we could
- // avoid clearing the BusyBits table. For now, always clear the
- // BusyBits table because deleting events is relatively rare.
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
- }
-
- // Read the start and end time for an event from the Events table.
- // Also read the "all-day" indicator.
- private TimeRange readEventStartEnd(long eventId) {
- Cursor cursor = null;
- TimeRange range = new TimeRange();
- try {
- cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- new String[] { Events.DTSTART, Events.DTEND, Events.ALL_DAY },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor == null || !cursor.moveToFirst()) {
- Log.d(TAG, "Couldn't find " + eventId + " in Events table");
- return null;
- }
- range.begin = cursor.getLong(0);
- range.end = cursor.getLong(1);
- range.allDay = cursor.getInt(2) != 0;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return range;
- }
-
- @Override
- public String getType(Uri url) {
- int match = sURLMatcher.match(url);
- switch (match) {
- case EVENTS:
- return "vnd.android.cursor.dir/event";
- case EVENTS_ID:
- return "vnd.android.cursor.item/event";
- case REMINDERS:
- return "vnd.android.cursor.dir/reminder";
- case REMINDERS_ID:
- return "vnd.android.cursor.item/reminder";
- case CALENDAR_ALERTS:
- return "vnd.android.cursor.dir/calendar-alert";
- case CALENDAR_ALERTS_BY_INSTANCE:
- return "vnd.android.cursor.dir/calendar-alert-by-instance";
- case CALENDAR_ALERTS_ID:
- return "vnd.android.cursor.item/calendar-alert";
- case INSTANCES:
- case INSTANCES_BY_DAY:
- return "vnd.android.cursor.dir/event-instance";
- case BUSYBITS:
- return "vnd.android.cursor.dir/busybits";
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
- }
-
- public static boolean isRecurrenceEvent(ContentValues values) {
- return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))||
- !TextUtils.isEmpty(values.getAsString(Events.RDATE))||
- !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)));
- }
-
- @Override
- public Uri insertInternal(Uri url, ContentValues initialValues) {
- final SQLiteDatabase db = getDatabase();
- long rowID;
-
- int match = sURLMatcher.match(url);
- switch (match) {
- case EVENTS:
- if (!isTemporary()) {
- initialValues.put(Events._SYNC_DIRTY, 1);
- if (!initialValues.containsKey(Events.DTSTART)) {
- throw new RuntimeException("DTSTART field missing from event");
- }
- }
- // TODO: avoid the call to updateBundleFromEvent if this is just finding local
- // changes. or avoid for temp providers altogether, if we can compute this
- // during a merge.
- // TODO: do we really need to make a copy?
- ContentValues updatedValues = updateContentValuesFromEvent(initialValues);
- if (updatedValues == null) {
- throw new RuntimeException("Could not insert event.");
- // return null;
- }
- String owner = null;
- if (updatedValues.containsKey(Events.CALENDAR_ID) &&
- !updatedValues.containsKey(Events.ORGANIZER)) {
- owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
- // TODO: This isn't entirely correct. If a guest is adding a recurrence
- // exception to an event, the organizer should stay the original organizer.
- // This value doesn't go to the server and it will get fixed on sync,
- // so it shouldn't really matter.
- if (owner != null) {
- updatedValues.put(Events.ORGANIZER, owner);
- }
- }
-
- long rowId = mEventsInserter.insert(updatedValues);
- Uri uri = Uri.parse("content://" + url.getAuthority() + "/events/" + rowId);
- if (!isTemporary() && rowId != -1) {
- updateEventRawTimesLocked(rowId, updatedValues);
- updateInstancesLocked(updatedValues, rowId, true /* new event */, db);
- insertBusyBitsLocked(rowId, updatedValues);
-
- // If we inserted a new event that specified the self-attendee
- // status, then we need to add an entry to the attendees table.
- if (initialValues.containsKey(Events.SELF_ATTENDEE_STATUS)) {
- int status = initialValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
- if (owner == null) {
- owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
- }
- createAttendeeEntry(rowId, status, owner);
- }
- triggerAppWidgetUpdate(rowId);
- }
-
- return uri;
- case CALENDARS:
- if (!isTemporary()) {
- Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
- if (syncEvents != null && syncEvents == 1) {
- String accountName = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
- String accountType = initialValues.getAsString(
- Calendars._SYNC_ACCOUNT_TYPE);
- final Account account = new Account(accountName, accountType);
- String calendarUrl = initialValues.getAsString(Calendars.URL);
- scheduleSync(account, false /* two-way sync */, calendarUrl);
- }
- }
- rowID = mCalendarsInserter.insert(initialValues);
- return ContentUris.withAppendedId(Calendars.CONTENT_URI, rowID);
- case ATTENDEES:
- if (!initialValues.containsKey(Attendees.EVENT_ID)) {
- throw new IllegalArgumentException("Attendees values must "
- + "contain an event_id");
- }
- rowID = mAttendeesInserter.insert(initialValues);
-
- // Copy the attendee status value to the Events table.
- updateEventAttendeeStatus(db, initialValues);
-
- return ContentUris.withAppendedId(Calendar.Attendees.CONTENT_URI, rowID);
- case REMINDERS:
- if (!initialValues.containsKey(Reminders.EVENT_ID)) {
- throw new IllegalArgumentException("Reminders values must "
- + "contain an event_id");
- }
- rowID = mRemindersInserter.insert(initialValues);
-
- if (!isTemporary()) {
- // Schedule another event alarm, if necessary
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "insertInternal() changing reminder");
- }
- scheduleNextAlarm(false /* do not remove alarms */);
- }
- return ContentUris.withAppendedId(Calendar.Reminders.CONTENT_URI, rowID);
- case CALENDAR_ALERTS:
- if (!initialValues.containsKey(CalendarAlerts.EVENT_ID)) {
- throw new IllegalArgumentException("CalendarAlerts values must "
- + "contain an event_id");
- }
- rowID = mCalendarAlertsInserter.insert(initialValues);
-
- return Uri.parse(CalendarAlerts.CONTENT_URI + "/" + rowID);
- case EXTENDED_PROPERTIES:
- if (!initialValues.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
- throw new IllegalArgumentException("ExtendedProperties values must "
- + "contain an event_id");
- }
- rowID = mExtendedPropertiesInserter.insert(initialValues);
-
- return ContentUris.withAppendedId(ExtendedProperties.CONTENT_URI, rowID);
- case DELETED_EVENTS:
- if (isTemporary()) {
- rowID = mDeletedEventsInserter.insert(initialValues);
- return ContentUris.withAppendedId(Calendar.Events.DELETED_CONTENT_URI, rowID);
- }
- // fallthrough
- case EVENTS_ID:
- case REMINDERS_ID:
- case CALENDAR_ALERTS_ID:
- case EXTENDED_PROPERTIES_ID:
- case INSTANCES:
- case INSTANCES_BY_DAY:
- throw new UnsupportedOperationException("Cannot insert into that URL");
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
- }
-
- /**
- * Gets the calendar's owner for an event.
- * @param calId
- * @return email of owner or null
- */
- private String getOwner(long calId) {
- // Get the email address of this user from this Calendar
- String emailAddress = null;
- Cursor cursor = null;
- try {
- cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
- new String[] { Calendars.OWNER_ACCOUNT },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor == null || !cursor.moveToFirst()) {
- Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
- return null;
- }
- emailAddress = cursor.getString(0);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- return emailAddress;
- }
-
- /**
- * Creates an entry in the Attendees table that refers to the given event
- * and that has the given response status.
- *
- * @param eventId the event id that the new entry in the Attendees table
- * should refer to
- * @param status the response status
- * @param emailAddress the email of the attendee
- */
- private void createAttendeeEntry(long eventId, int status, String emailAddress) {
- ContentValues values = new ContentValues();
- values.put(Attendees.EVENT_ID, eventId);
- values.put(Attendees.ATTENDEE_STATUS, status);
- values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
- // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
- // on sync.
- values.put(Attendees.ATTENDEE_RELATIONSHIP,
- Attendees.RELATIONSHIP_ATTENDEE);
- values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
-
- // We don't know the ATTENDEE_NAME but that will be filled in by the
- // server and sent back to us.
- mAttendeesInserter.insert(values);
- }
-
- /**
- * Updates the attendee status in the Events table to be consistent with
- * the value in the Attendees table.
- *
- * @param db the database
- * @param attendeeValues the column values for one row in the Attendees
- * table.
- */
- private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
- // Get the event id for this attendee
- long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
-
- if (MULTIPLE_ATTENDEES_PER_EVENT) {
- // Get the calendar id for this event
- Cursor cursor = null;
- long calId;
- try {
- cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- new String[] { Events.CALENDAR_ID },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor == null || !cursor.moveToFirst()) {
- Log.d(TAG, "Couldn't find " + eventId + " in Events table");
- return;
- }
- calId = cursor.getLong(0);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- // Get the owner email for this Calendar
- String calendarEmail = null;
- cursor = null;
- try {
- cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
- new String[] { Calendars.OWNER_ACCOUNT },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor == null || !cursor.moveToFirst()) {
- Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
- return;
- }
- calendarEmail = cursor.getString(0);
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- if (calendarEmail == null) {
- return;
- }
-
- // Get the email address for this attendee
- String attendeeEmail = null;
- if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
- attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
- }
-
- // If the attendee email does not match the calendar email, then this
- // attendee is not the owner of this calendar so we don't update the
- // selfAttendeeStatus in the event.
- if (!calendarEmail.equals(attendeeEmail)) {
- return;
- }
- }
-
- int status = Attendees.ATTENDEE_STATUS_NONE;
- if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
- int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
- if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
- status = Attendees.ATTENDEE_STATUS_ACCEPTED;
- }
- }
-
- if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
- status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
- }
-
- ContentValues values = new ContentValues();
- values.put(Events.SELF_ATTENDEE_STATUS, status);
- db.update("Events", values, "_id="+eventId, null);
- }
-
- /**
- * Updates the instances table when an event is added or updated.
- * @param values The new values of the event.
- * @param rowId The database row id of the event.
- * @param newEvent true if the event is new.
- * @param db The database
- */
- private void updateInstancesLocked(ContentValues values,
- long rowId,
- boolean newEvent,
- SQLiteDatabase db) {
-
- // If there are no expanded Instances, then return.
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- if (fields.maxInstance == 0) {
- return;
- }
-
- Long dtstartMillis = values.getAsLong(Events.DTSTART);
- if (dtstartMillis == null) {
- if (newEvent) {
- // must be present for a new event.
- throw new RuntimeException("DTSTART missing.");
- }
- if (Config.LOGV) Log.v(TAG, "Missing DTSTART. "
- + "No need to update instance.");
- return;
- }
-
- Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
- Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
-
- if (!newEvent) {
- // Want to do this for regular event, recurrence, or exception.
- // For recurrence or exception, more deletion may happen below if we
- // do an instance expansion. This deletion will suffice if the exception
- // is moved outside the window, for instance.
- db.delete("Instances", "event_id=" + rowId, null /* selectionArgs */);
- }
-
- if (isRecurrenceEvent(values)) {
- // The recurrence or exception needs to be (re-)expanded if:
- // a) Exception or recurrence that falls inside window
- boolean insideWindow = dtstartMillis <= fields.maxInstance &&
- (lastDateMillis == null || lastDateMillis >= fields.minInstance);
- // b) Exception that affects instance inside window
- // These conditions match the query in getEntries
- // See getEntries comment for explanation of subtracting 1 week.
- boolean affectsWindow = originalInstanceTime != null &&
- originalInstanceTime <= fields.maxInstance &&
- originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
- if (insideWindow || affectsWindow) {
- updateRecurrenceInstancesLocked(values, rowId, db);
- }
- // TODO: an exception creation or update could be optimized by
- // updating just the affected instances, instead of regenerating
- // the recurrence.
- return;
- }
-
- Long dtendMillis = values.getAsLong(Events.DTEND);
- if (dtendMillis == null) {
- dtendMillis = dtstartMillis;
- }
-
- // if the event is in the expanded range, insert
- // into the instances table.
- // TODO: deal with durations. currently, durations are only used in
- // recurrences.
-
- if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
- ContentValues instanceValues = new ContentValues();
- instanceValues.put(Instances.EVENT_ID, rowId);
- instanceValues.put(Instances.BEGIN, dtstartMillis);
- instanceValues.put(Instances.END, dtendMillis);
-
- boolean allDay = false;
- Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
- if (allDayInteger != null) {
- allDay = allDayInteger != 0;
- }
-
- // Update the timezone-dependent fields.
- Time local = new Time();
- if (allDay) {
- local.timezone = Time.TIMEZONE_UTC;
- } else {
- local.timezone = fields.timezone;
- }
-
- computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
- mInstancesInserter.insert(instanceValues);
- }
- }
-
- /**
- * Determines the recurrence entries associated with a particular recurrence.
- * This set is the base recurrence and any exception.
- *
- * Normally the entries are indicated by the sync id of the base recurrence
- * (which is the originalEvent in the exceptions).
- * However, a complication is that a recurrence may not yet have a sync id.
- * In that case, the recurrence is specified by the rowId.
- *
- * @param recurrenceSyncId The sync id of the base recurrence, or null.
- * @param rowId The row id of the base recurrence.
- * @return the relevant entries.
- */
- private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
- final SQLiteDatabase db = getDatabase();
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
- qb.setTables("Events INNER JOIN Calendars ON (calendar_id = Calendars._id)");
- qb.setProjectionMap(sEventsProjectionMap);
- if (recurrenceSyncId == null) {
- String where = "Events._id = " + rowId;
- qb.appendWhere(where);
- } else {
- String where = "Events._sync_id = \"" + recurrenceSyncId + "\""
- + " OR Events.originalEvent = \"" + recurrenceSyncId + "\"";
- qb.appendWhere(where);
- }
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Retrieving events to expand: " + qb.toString());
- }
-
- return qb.query(db, EXPAND_COLUMNS, null /* selection */, null /* selectionArgs */, null /* groupBy */, null /* having */, null /* sortOrder */);
- }
-
- /**
- * Do incremental Instances update of a recurrence or recurrence exception.
- *
- * This method does performInstanceExpansion on just the modified recurrence,
- * to avoid the overhead of recomputing the entire instance table.
- *
- * @param values The new values of the event.
- * @param rowId The database row id of the event.
- * @param db The database
- */
- private void updateRecurrenceInstancesLocked(ContentValues values,
- long rowId,
- SQLiteDatabase db) {
- MetaData.Fields fields = mMetaData.getFieldsLocked();
- String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
- String recurrenceSyncId = null;
- if (originalEvent != null) {
- recurrenceSyncId = originalEvent;
- } else {
- // Get the recurrence's sync id from the database
- recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
- + " WHERE _id = " + rowId, null /* selection args */);
- }
- // recurrenceSyncId is the _sync_id of the underlying recurrence
- // If the recurrence hasn't gone to the server, it will be null.
-
- // Need to clear out old instances
- if (recurrenceSyncId == null) {
- // Creating updating a recurrence that hasn't gone to the server.
- // Need to delete based on row id
- String where = "_id IN (SELECT Instances._id as _id"
- + " FROM Instances INNER JOIN Events"
- + " ON (Events._id = Instances.event_id)"
- + " WHERE Events._id =?)";
- db.delete("Instances", where, new String[]{"" + rowId});
- } else {
- // Creating or modifying a recurrence or exception.
- // Delete instances for recurrence (_sync_id = recurrenceSyncId)
- // and all exceptions (originalEvent = recurrenceSyncId)
- String where = "_id IN (SELECT Instances._id as _id"
- + " FROM Instances INNER JOIN Events"
- + " ON (Events._id = Instances.event_id)"
- + " WHERE Events._sync_id =?"
- + " OR Events.originalEvent =?)";
- db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
- }
-
- // Now do instance expansion
- Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
- try {
- performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone, entries);
- } finally {
- if (entries != null) {
- entries.close();
- }
- }
-
- // Clear busy bits
- mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
- }
-
- long calculateLastDate(ContentValues values)
- throws DateException {
- // Allow updates to some event fields like the title or hasAlarm
- // without requiring DTSTART.
- if (!values.containsKey(Events.DTSTART)) {
- if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
- || values.containsKey(Events.DURATION)
- || values.containsKey(Events.EVENT_TIMEZONE)
- || values.containsKey(Events.RDATE)
- || values.containsKey(Events.EXRULE)
- || values.containsKey(Events.EXDATE)) {
- throw new RuntimeException("DTSTART field missing from event");
- }
- return -1;
- }
- long dtstartMillis = values.getAsLong(Events.DTSTART);
- long lastMillis = -1;
-
- // Can we use dtend with a repeating event? What does that even
- // mean?
- // NOTE: if the repeating event has a dtend, we convert it to a
- // duration during event processing, so this situation should not
- // occur.
- Long dtEnd = values.getAsLong(Events.DTEND);
- if (dtEnd != null) {
- lastMillis = dtEnd;
- } else {
- // find out how long it is
- Duration duration = new Duration();
- String durationStr = values.getAsString(Events.DURATION);
- if (durationStr != null) {
- duration.parse(durationStr);
- }
-
- RecurrenceSet recur = new RecurrenceSet(values);
-
- if (recur.hasRecurrence()) {
- // the event is repeating, so find the last date it
- // could appear on
-
- String tz = values.getAsString(Events.EVENT_TIMEZONE);
-
- if (TextUtils.isEmpty(tz)) {
- // floating timezone
- tz = Time.TIMEZONE_UTC;
- }
- Time dtstartLocal = new Time(tz);
-
- dtstartLocal.set(dtstartMillis);
-
- RecurrenceProcessor rp = new RecurrenceProcessor();
- lastMillis = rp.getLastOccurence(dtstartLocal, recur);
- if (lastMillis == -1) {
- return lastMillis; // -1
- }
- } else {
- // the event is not repeating, just use dtstartMillis
- lastMillis = dtstartMillis;
- }
-
- // that was the beginning of the event. this is the end.
- lastMillis = duration.addTo(lastMillis);
- }
- return lastMillis;
- }
-
- private ContentValues updateContentValuesFromEvent(ContentValues initialValues) {
- try {
- ContentValues values = new ContentValues(initialValues);
-
- long last = calculateLastDate(values);
- if (last != -1) {
- values.put(Events.LAST_DATE, last);
- }
-
- return values;
- } catch (DateException e) {
- // don't add it if there was an error
- Log.w(TAG, "Could not calculate last date.", e);
- return null;
- }
- }
-
- private void updateEventRawTimesLocked(long eventId, ContentValues values) {
- ContentValues rawValues = new ContentValues();
-
- rawValues.put("event_id", eventId);
-
- String timezone = values.getAsString(Events.EVENT_TIMEZONE);
-
- boolean allDay = false;
- Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
- if (allDayInteger != null) {
- allDay = allDayInteger != 0;
- }
-
- if (allDay || TextUtils.isEmpty(timezone)) {
- // floating timezone
- timezone = Time.TIMEZONE_UTC;
- }
-
- Time time = new Time(timezone);
- time.allDay = allDay;
- Long dtstartMillis = values.getAsLong(Events.DTSTART);
- if (dtstartMillis != null) {
- time.set(dtstartMillis);
- rawValues.put("dtstart2445", time.format2445());
- }
-
- Long dtendMillis = values.getAsLong(Events.DTEND);
- if (dtendMillis != null) {
- time.set(dtendMillis);
- rawValues.put("dtend2445", time.format2445());
- }
-
- Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- if (originalInstanceMillis != null) {
- // This is a recurrence exception so we need to get the all-day
- // status of the original recurring event in order to format the
- // date correctly.
- allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
- if (allDayInteger != null) {
- time.allDay = allDayInteger != 0;
- }
- time.set(originalInstanceMillis);
- rawValues.put("originalInstanceTime2445", time.format2445());
- }
-
- Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
- if (lastDateMillis != null) {
- time.allDay = allDay;
- time.set(lastDateMillis);
- rawValues.put("lastDate2445", time.format2445());
- }
-
- mEventsRawTimesInserter.replace(rawValues);
- }
-
- @Override
- public int deleteInternal(Uri url, String where, String[] whereArgs) {
- final SQLiteDatabase db = getDatabase();
- int match = sURLMatcher.match(url);
- switch (match)
- {
- case EVENTS_ID:
- {
- String id = url.getLastPathSegment();
- if (where != null) {
- throw new UnsupportedOperationException("CalendarProvider "
- + "doesn't support where based deletion for type "
- + match);
- }
- if (!isTemporary()) {
- deleteBusyBitsLocked(Integer.parseInt(id));
-
- // Query this event to get the fields needed for inserting
- // a new row in the DeletedEvents table.
- Cursor cursor = db.query("Events", EVENTS_PROJECTION,
- "_id=" + id, null, null, null, null);
- try {
- if (cursor.moveToNext()) {
- String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
- if (!TextUtils.isEmpty(syncId)) {
- String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
- String syncAccountName =
- cursor.getString(EVENTS_SYNC_ACCOUNT_NAME_INDEX);
- String syncAccountType =
- cursor.getString(EVENTS_SYNC_ACCOUNT_TYPE_INDEX);
- Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);
-
- ContentValues values = new ContentValues();
- values.put(Events._SYNC_ID, syncId);
- values.put(Events._SYNC_VERSION, syncVersion);
- values.put(Events._SYNC_ACCOUNT, syncAccountName);
- values.put(Events._SYNC_ACCOUNT_TYPE, syncAccountType);
- values.put(Events.CALENDAR_ID, calId);
- mDeletedEventsInserter.insert(values);
-
- // TODO: we may also want to delete exception
- // events for this event (in case this was a
- // recurring event). We can do that with the
- // following code:
- // db.delete("Events", "originalEvent=?", new String[] {syncId});
- }
-
- // If this was a recurring event or a recurrence
- // exception, then force a recalculation of the
- // instances.
- String rrule = cursor.getString(EVENTS_RRULE_INDEX);
- String rdate = cursor.getString(EVENTS_RDATE_INDEX);
- String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
- if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
- || !TextUtils.isEmpty(origEvent)) {
- mMetaData.clearInstanceRange();
- }
- }
- } finally {
- cursor.close();
- cursor = null;
- }
- triggerAppWidgetUpdate(-1);
- }
-
- // There is a delete trigger that will cause all instances
- // matching this event id to get deleted as well. In fact, all
- // of the following tables will remove entries matching this
- // event id: Instances, EventsRawTimes, Attendees, Reminders,
- // CalendarAlerts, and ExtendedProperties.
- int result = db.delete("Events", "_id=" + id, null);
- return result;
- }
- case ATTENDEES:
- {
- int result = db.delete("Attendees", where, whereArgs);
- return result;
- }
- case ATTENDEES_ID:
- {
- // we currently don't support deletions to the attendees list.
- // TODO: remove this restriction when we handle the full attendees
- // feed. we'll need to put in some logic to check that the
- // modification will be allowed by the server.
- throw new IllegalArgumentException("Cannot delete attendees.");
- // String id = url.getPathSegments().get(1);
- // int result = db.delete("Attendees", "_id="+id, null);
- // return result;
- }
- case REMINDERS:
- {
- int result = db.delete("Reminders", where, whereArgs);
- return result;
- }
- case REMINDERS_ID:
- {
- String id = url.getLastPathSegment();
- int result = db.delete("Reminders", "_id="+id, null);
- return result;
- }
- case CALENDAR_ALERTS:
- {
- int result = db.delete("CalendarAlerts", where, whereArgs);
- return result;
- }
- case CALENDAR_ALERTS_ID:
- {
- String id = url.getLastPathSegment();
- int result = db.delete("CalendarAlerts", "_id="+id, null);
- return result;
- }
- case DELETED_EVENTS:
- case EVENTS:
- throw new UnsupportedOperationException("Cannot delete that URL");
- case CALENDARS_ID:
- StringBuilder whereSb = new StringBuilder("_id=");
- whereSb.append(url.getPathSegments().get(1));
- if (!TextUtils.isEmpty(where)) {
- whereSb.append(" AND (");
- whereSb.append(where);
- whereSb.append(')');
- }
- where = whereSb.toString();
- // fall through to CALENDARS for the actual delete
- case CALENDARS:
- return deleteMatchingCalendars(where);
- case INSTANCES:
- case INSTANCES_BY_DAY:
- throw new UnsupportedOperationException("Cannot delete that URL");
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
- }
-
- private int deleteMatchingCalendars(String where) {
- // query to find all the calendars that match, for each
- // - delete calendar subscription
- // - delete calendar
-
- int numDeleted = 0;
- final SQLiteDatabase db = getDatabase();
- Cursor c = db.query("Calendars", sCalendarsIdProjection, where, null,
- null, null, null);
- if (c == null) {
- return 0;
- }
- try {
- while (c.moveToNext()) {
- long id = c.getLong(CALENDARS_INDEX_ID);
- if (!isTemporary()) {
- modifyCalendarSubscription(id, false /* not selected */);
- }
- c.deleteRow();
- numDeleted++;
- }
- } finally {
- c.close();
- }
- return numDeleted;
- }
-
- // TODO: call calculateLastDate()!
- @Override
- public int updateInternal(Uri url, ContentValues values,
- String where, String[] selectionArgs) {
- int match = sURLMatcher.match(url);
-
- // TODO: remove this restriction
- if (!TextUtils.isEmpty(where) && match != CALENDAR_ALERTS) {
- throw new IllegalArgumentException(
- "WHERE based updates not supported");
- }
- final SQLiteDatabase db = getDatabase();
-
- switch (match) {
- case CALENDARS_ID:
- {
- long id = ContentUris.parseId(url);
- Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
- if (syncEvents != null && !isTemporary()) {
- modifyCalendarSubscription(id, syncEvents == 1);
- }
-
- int result = db.update("Calendars", values, "_id="+ id, null);
- if (!isTemporary()) {
- // When we change the display status of a Calendar
- // we need to update the busy bits.
- if (values.containsKey(Calendars.SELECTED) || (syncEvents != null)) {
- // Clear the BusyBits table.
- mMetaData.clearBusyBitRange();
- }
- }
-
- return result;
- }
- case EVENTS_ID:
- {
- long id = ContentUris.parseId(url);
- if (!isTemporary()) {
- values.put(Events._SYNC_DIRTY, 1);
-
- // Disallow updating the attendee status in the Events
- // table. In the future, we could support this but we
- // would have to query and update the attendees table
- // to keep the values consistent.
- if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
- throw new IllegalArgumentException("Updating "
- + Events.SELF_ATTENDEE_STATUS
- + " in Events table is not allowed.");
- }
-
- if (values.containsKey(Events.HTML_URI)) {
- throw new IllegalArgumentException("Updating "
- + Events.HTML_URI
- + " in Events table is not allowed.");
- }
-
- updateBusyBitsLocked(id, values);
- }
-
- ContentValues updatedValues = updateContentValuesFromEvent(values);
- if (updatedValues == null) {
- Log.w(TAG, "Could not update event.");
- return 0;
- }
-
- int result = db.update("Events", updatedValues, "_id="+id, null);
- if (!isTemporary()) {
- if (result > 0) {
- updateEventRawTimesLocked(id, updatedValues);
- updateInstancesLocked(updatedValues, id, false /* not a new event */, db);
-
- if (values.containsKey(Events.DTSTART)) {
- // The start time of the event changed, so run the
- // event alarm scheduler.
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "updateInternal() changing event");
- }
- scheduleNextAlarm(false /* do not remove alarms */);
- triggerAppWidgetUpdate(id);
- }
- }
- }
- return result;
- }
- case ATTENDEES_ID:
- {
- // Copy the attendee status value to the Events table.
- updateEventAttendeeStatus(db, values);
-
- long id = ContentUris.parseId(url);
- return db.update("Attendees", values, "_id="+id, null);
- }
- case CALENDAR_ALERTS_ID:
- {
- long id = ContentUris.parseId(url);
- return db.update("CalendarAlerts", values, "_id="+id, null);
- }
- case CALENDAR_ALERTS:
- {
- return db.update("CalendarAlerts", values, where, null);
- }
- case REMINDERS_ID:
- {
- long id = ContentUris.parseId(url);
- int result = db.update("Reminders", values, "_id="+id, null);
- if (!isTemporary()) {
- // Reschedule the event alarms because the
- // "minutes" field may have changed.
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "updateInternal() changing reminder");
- }
- scheduleNextAlarm(false /* do not remove alarms */);
- }
- return result;
- }
- case EXTENDED_PROPERTIES_ID:
- {
- long id = ContentUris.parseId(url);
- return db.update("ExtendedProperties", values, "_id="+id, null);
- }
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
- }
-
- /**
- * Schedule a calendar sync for the account.
- * @param account the account for which to schedule a sync
- * @param uploadChangesOnly if set, specify that the sync should only send
- * up local changes
- * @param url the url feed for the calendar to sync (may be null)
- */
- private void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
- Bundle extras = new Bundle();
- extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
- if (url != null) {
- extras.putString("feed", url);
- extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
- }
- ContentResolver.requestSync(account, Calendars.CONTENT_URI.getAuthority(), extras);
- }
-
- private void modifyCalendarSubscription(long id, boolean syncEvents) {
- // get the account, url, and current selected state
- // for this calendar.
- Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
- new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
- Calendars.URL, Calendars.SYNC_EVENTS},
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
-
- Account account = null;
- String calendarUrl = null;
- boolean oldSyncEvents = false;
- if (cursor != null && cursor.moveToFirst()) {
- try {
- final String accountName = cursor.getString(0);
- final String accountType = cursor.getString(1);
- account = new Account(accountName, accountType);
- calendarUrl = cursor.getString(2);
- oldSyncEvents = (cursor.getInt(3) != 0);
- } finally {
- cursor.close();
- }
- }
-
- if (account == null || TextUtils.isEmpty(calendarUrl)) {
- // should not happen?
- Log.w(TAG, "Cannot update subscription because account "
- + "or calendar url empty -- should not happen.");
- return;
- }
-
- if (oldSyncEvents == syncEvents) {
- // nothing to do
- return;
- }
-
- // If we are no longer syncing a calendar then make sure that the
- // old calendar sync data is cleared. Then if we later add this
- // calendar back, we will sync all the events.
- if (!syncEvents) {
- byte[] data = readSyncDataBytes(account);
- GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data);
- if (syncData != null) {
- syncData.feedData.remove(calendarUrl);
- data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData);
- writeSyncDataBytes(account, data);
- }
-
- // Delete all of the events in this calendar to save space.
- // This is the closest we can come to deleting a calendar.
- // Clients should never actually delete a calendar. That won't
- // work. We need to keep the calendar entry in the Calendars table
- // in order to know not to sync the events for that calendar from
- // the server.
- final SQLiteDatabase db = getDatabase();
- String[] args = new String[] {Long.toString(id)};
- db.delete("Events", CALENDAR_ID_SELECTION, args);
- // Note that we do not delete the matching entries
- // in the DeletedEvents table. We will let those
- // deleted events propagate to the server.
-
- // TODO: cancel any pending/ongoing syncs for this calendar.
-
- // TODO: there is a corner case to deal with here: namely, if
- // we edit or delete an event on the phone and then remove
- // (that is, stop syncing) a calendar, and if we also make a
- // change on the server to that event at about the same time,
- // then we will never propagate the changes from the phone to
- // the server.
- }
-
- // If the calendar is not selected for syncing, then don't download
- // events.
- scheduleSync(account, !syncEvents, calendarUrl);
- }
-
- @Override
- public void onSyncStop(SyncContext context, boolean success) {
- super.onSyncStop(context, success);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onSyncStop() success: " + success);
- }
- scheduleNextAlarm(false /* do not remove alarms */);
- triggerAppWidgetUpdate(-1);
- }
-
- @Override
- protected Iterable<EventMerger> getMergers() {
- return Collections.singletonList(new EventMerger());
- }
-
- /**
- * Update any existing widgets with the changed events.
- *
- * @param changedEventId Specific event known to be changed, otherwise -1.
- * If present, we use it to decide if an update is necessary.
- */
- private synchronized void triggerAppWidgetUpdate(long changedEventId) {
- Context context = getContext();
- if (context != null) {
- mAppWidgetProvider.providerUpdated(context, changedEventId);
- }
- }
-
- void bootCompleted() {
- // Remove alarms from the CalendarAlerts table that have been marked
- // as "scheduled" but not fired yet. We do this because the
- // AlarmManagerService loses all information about alarms when the
- // power turns off but we store the information in a database table
- // that persists across reboots. See the documentation for
- // scheduleNextAlarmLocked() for more information.
- scheduleNextAlarm(true /* remove alarms */);
- }
-
- /* Retrieve and cache the alarm manager */
- private AlarmManager getAlarmManager() {
- synchronized(mAlarmLock) {
- if (mAlarmManager == null) {
- Context context = getContext();
- if (context == null) {
- Log.e(TAG, "getAlarmManager() cannot get Context");
- return null;
- }
- Object service = context.getSystemService(Context.ALARM_SERVICE);
- mAlarmManager = (AlarmManager) service;
- }
- return mAlarmManager;
- }
- }
-
- void scheduleNextAlarmCheck(long triggerTime) {
- AlarmManager manager = getAlarmManager();
- if (manager == null) {
- Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
- return;
- }
- Context context = getContext();
- Intent intent = new Intent(CalendarReceiver.SCHEDULE);
- intent.setClass(context, CalendarReceiver.class);
- PendingIntent pending = PendingIntent.getBroadcast(context,
- 0, intent, PendingIntent.FLAG_NO_CREATE);
- if (pending != null) {
- // Cancel any previous alarms that do the same thing.
- manager.cancel(pending);
- }
- pending = PendingIntent.getBroadcast(context,
- 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Time time = new Time();
- time.set(triggerTime);
- String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
- }
-
- manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
- }
-
- /*
- * This method runs the alarm scheduler in a background thread.
- */
- void scheduleNextAlarm(boolean removeAlarms) {
- Thread thread = new AlarmScheduler(removeAlarms);
- thread.start();
- }
-
- /**
- * This method runs in a background thread and schedules an alarm for
- * the next calendar event, if necessary.
- */
- private void runScheduleNextAlarm(boolean removeAlarms) {
- // Do not schedule any alarms if this is a temporary database.
- if (isTemporary()) {
- return;
- }
-
- final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- if (removeAlarms) {
- removeScheduledAlarmsLocked(db);
- }
- scheduleNextAlarmLocked(db);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- /**
- * This method looks at the 24-hour window from now for any events that it
- * needs to schedule. This method runs within a database transaction. It
- * also runs in a background thread.
- *
- * The CalendarProvider keeps track of which alarms it has already scheduled
- * to avoid scheduling them more than once and for debugging problems with
- * alarms. It stores this knowledge in a database table called CalendarAlerts
- * which persists across reboots. But the actual alarm list is in memory
- * and disappears if the phone loses power. To avoid missing an alarm, we
- * clear the entries in the CalendarAlerts table when we start up the
- * CalendarProvider.
- *
- * Scheduling an alarm multiple times is not tragic -- we filter out the
- * extra ones when we receive them. But we still need to keep track of the
- * scheduled alarms. The main reason is that we need to prevent multiple
- * notifications for the same alarm (on the receive side) in case we
- * accidentally schedule the same alarm multiple times. We don't have
- * visibility into the system's alarm list so we can never know for sure if
- * we have already scheduled an alarm and it's better to err on scheduling
- * an alarm twice rather than missing an alarm. Another reason we keep
- * track of scheduled alarms in a database table is that it makes it easy to
- * run an SQL query to find the next reminder that we haven't scheduled.
- *
- * @param db the database
- */
- private void scheduleNextAlarmLocked(SQLiteDatabase db) {
- AlarmManager alarmManager = getAlarmManager();
- if (alarmManager == null) {
- Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
- return;
- }
-
- final long currentMillis = System.currentTimeMillis();
- final long start = currentMillis - SCHEDULE_ALARM_SLACK;
- final long end = start + (24 * 60 * 60 * 1000);
- ContentResolver cr = getContext().getContentResolver();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Time time = new Time();
- time.set(start);
- String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
- }
-
- // Clear old alarms but keep alarms around for a while to prevent
- // multiple alerts for the same reminder. The "clearUpToTime'
- // should be further in the past than the point in time where
- // we start searching for events (the "start" variable defined above).
- long clearUpToTime = currentMillis - CLEAR_OLD_ALARM_THRESHOLD;
- db.delete("CalendarAlerts", CalendarAlerts.ALARM_TIME + "<" + clearUpToTime, null);
-
- long nextAlarmTime = end;
- long alarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
- if (alarmTime != -1 && alarmTime < nextAlarmTime) {
- nextAlarmTime = alarmTime;
- }
-
- // Extract events from the database sorted by alarm time. The
- // alarm times are computed from Instances.begin (whose units
- // are milliseconds) and Reminders.minutes (whose units are
- // minutes).
- //
- // Also, ignore events whose end time is already in the past.
- // Also, ignore events alarms that we have already scheduled.
- //
- // Note 1: we can add support for the case where Reminders.minutes
- // equals -1 to mean use Calendars.minutes by adding a UNION for
- // that case where the two halves restrict the WHERE clause on
- // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
- //
- // Note 2: we have to name "myAlarmTime" different from the
- // "alarmTime" column in CalendarAlerts because otherwise the
- // query won't find multiple alarms for the same event.
- String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
- + " Instances.event_id AS eventId, begin, end,"
- + " title, allDay, method, minutes"
- + " FROM Instances INNER JOIN Events"
- + " ON (Events._id = Instances.event_id)"
- + " INNER JOIN Reminders"
- + " ON (Instances.event_id = Reminders.event_id)"
- + " WHERE method=" + Reminders.METHOD_ALERT
- + " AND myAlarmTime>=" + start
- + " AND myAlarmTime<=" + nextAlarmTime
- + " AND end>=" + currentMillis
- + " AND 0=(SELECT count(*) from CalendarAlerts CA"
- + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
- + " AND CA.alarmTime=myAlarmTime)"
- + " ORDER BY myAlarmTime,begin,title";
-
- acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */);
- Cursor cursor = null;
- try {
- cursor = db.rawQuery(query, null);
-
- int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
- int endIndex = cursor.getColumnIndex(Instances.END);
- int eventIdIndex = cursor.getColumnIndex("eventId");
- int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
- int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Time time = new Time();
- time.set(nextAlarmTime);
- String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(TAG, "nextAlarmTime: " + alarmTimeStr
- + " cursor results: " + cursor.getCount()
- + " query: " + query);
- }
-
- while (cursor.moveToNext()) {
- // Schedule all alarms whose alarm time is as early as any
- // scheduled alarm. For example, if the earliest alarm is at
- // 1pm, then we will schedule all alarms that occur at 1pm
- // but no alarms that occur later than 1pm.
- // Actually, we allow alarms up to a minute later to also
- // be scheduled so that we don't have to check immediately
- // again after an event alarm goes off.
- alarmTime = cursor.getLong(alarmTimeIndex);
- long eventId = cursor.getLong(eventIdIndex);
- int minutes = cursor.getInt(minutesIndex);
- long startTime = cursor.getLong(beginIndex);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- int titleIndex = cursor.getColumnIndex(Events.TITLE);
- String title = cursor.getString(titleIndex);
- Time time = new Time();
- time.set(alarmTime);
- String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
- time.set(startTime);
- String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- long endTime = cursor.getLong(endIndex);
- time.set(endTime);
- String endTimeStr = time.format(" - %a, %b %d, %Y %I:%M%P");
- time.set(currentMillis);
- String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(TAG, " looking at id: " + eventId + " " + title
- + " " + startTime
- + startTimeStr + endTimeStr + " alarm: "
- + alarmTime + schedTime
- + " currentTime: " + currentTimeStr);
- }
-
- if (alarmTime < nextAlarmTime) {
- nextAlarmTime = alarmTime;
- } else if (alarmTime > nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS) {
- // This event alarm (and all later ones) will be scheduled
- // later.
- break;
- }
-
- // Avoid an SQLiteContraintException by checking if this alarm
- // already exists in the table.
- if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- int titleIndex = cursor.getColumnIndex(Events.TITLE);
- String title = cursor.getString(titleIndex);
- Log.d(TAG, " alarm exists for id: " + eventId + " " + title);
- }
- continue;
- }
-
- // Insert this alarm into the CalendarAlerts table
- long endTime = cursor.getLong(endIndex);
- Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
- endTime, alarmTime, minutes);
- if (uri == null) {
- Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed");
- continue;
- }
-
- Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION);
- intent.setData(uri);
-
- // Also include the begin and end time of this event, because
- // we cannot determine that from the Events database table.
- intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, startTime);
- intent.putExtra(android.provider.Calendar.EVENT_END_TIME, endTime);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- int titleIndex = cursor.getColumnIndex(Events.TITLE);
- String title = cursor.getString(titleIndex);
- Time time = new Time();
- time.set(alarmTime);
- String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
- time.set(startTime);
- String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- time.set(endTime);
- String endTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- time.set(currentMillis);
- String currentTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
- Log.d(TAG, " scheduling " + title
- + startTimeStr + " - " + endTimeStr + " alarm: " + schedTime
- + " currentTime: " + currentTimeStr
- + " uri: " + uri);
- }
- PendingIntent sender = PendingIntent.getBroadcast(getContext(),
- 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- // If we scheduled an event alarm, then schedule the next alarm check
- // for one minute past that alarm. Otherwise, if there were no
- // event alarms scheduled, then check again in 24 hours. If a new
- // event is inserted before the next alarm check, then this method
- // will be run again when the new event is inserted.
- if (nextAlarmTime != Long.MAX_VALUE) {
- scheduleNextAlarmCheck(nextAlarmTime + android.text.format.DateUtils.MINUTE_IN_MILLIS);
- } else {
- scheduleNextAlarmCheck(currentMillis + android.text.format.DateUtils.DAY_IN_MILLIS);
- }
- }
-
- /**
- * Removes the entries in the CalendarAlerts table for alarms that we have
- * scheduled but that have not fired yet. We do this to ensure that we
- * don't miss an alarm. The CalendarAlerts table keeps track of the
- * alarms that we have scheduled but the actual alarm list is in memory
- * and will be cleared if the phone reboots.
- *
- * We don't need to remove entries that have already fired, and in fact
- * we should not remove them because we need to display the notifications
- * until the user dismisses them.
- *
- * We could remove entries that have fired and been dismissed, but we leave
- * them around for a while because it makes it easier to debug problems.
- * Entries that are old enough will be cleaned up later when we schedule
- * new alarms.
- */
- private void removeScheduledAlarmsLocked(SQLiteDatabase db) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "removing scheduled alarms");
- }
- db.delete(CalendarAlerts.TABLE_NAME,
- CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
- }
-
- private static String sEventsTable = "Events";
- private static String sDeletedEventsTable = "DeletedEvents";
- private static String sAttendeesTable = "Attendees";
- private static String sRemindersTable = "Reminders";
- private static String sCalendarAlertsTable = "CalendarAlerts";
- private static String sExtendedPropertiesTable = "ExtendedProperties";
-
- private class EventMerger extends AbstractTableMerger {
-
- private ContentValues mValues = new ContentValues();
- EventMerger() {
- super(getDatabase(), sEventsTable, Calendar.Events.CONTENT_URI,
- sDeletedEventsTable, Calendar.Events.DELETED_CONTENT_URI);
- }
-
- @Override
- protected void notifyChanges() {
- getContext().getContentResolver().notifyChange(Events.CONTENT_URI,
- null /* observer */, false /* do not sync to network */);
- }
-
- @Override
- protected void cursorRowToContentValues(Cursor cursor, ContentValues map) {
- rowToContentValues(cursor, map);
- }
-
- @Override
- public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
- rowToContentValues(diffsCursor, mValues);
- final SQLiteDatabase db = getDatabase();
- long rowId = mEventsInserter.insert(mValues);
- if (rowId <= 0) {
- Log.e(TAG, "Unable to insert values into calendar db: " + mValues);
- return;
- }
-
- long diffsRowId = diffsCursor.getLong(
- diffsCursor.getColumnIndex(Events._ID));
-
- insertAttendees(diffs, diffsRowId, rowId, db);
- insertRemindersIfNecessary(diffs, diffsRowId, rowId, db);
- insertExtendedPropertiesIfNecessary(diffs, diffsRowId, rowId, db);
- updateEventRawTimesLocked(rowId, mValues);
- updateInstancesLocked(mValues, rowId, true /* new event */, db);
- insertBusyBitsLocked(rowId, mValues);
-
- // Update the _SYNC_DIRTY flag of the event. We have to do this
- // after inserting since the update of the reminders and extended properties
- // methods will fire a sql trigger that will cause this flag to
- // be set.
- clearSyncDirtyFlag(db, rowId);
- }
-
- private void clearSyncDirtyFlag(SQLiteDatabase db, long rowId) {
- mValues.clear();
- mValues.put(Events._SYNC_DIRTY, 0);
- db.update(mTable, mValues, Events._ID + '=' + rowId, null);
- }
-
- private void insertAttendees(ContentProvider diffs,
- long diffsRowId,
- long rowId,
- SQLiteDatabase db) {
- // query attendees in diffs
- Cursor attendeesCursor =
- diffs.query(Attendees.CONTENT_URI, null,
- "event_id=" + diffsRowId, null, null);
- ContentValues attendeesValues = new ContentValues();
- try {
- while (attendeesCursor.moveToNext()) {
- attendeesValues.clear();
- DatabaseUtils.cursorStringToContentValues(attendeesCursor,
- Attendees.ATTENDEE_NAME,
- attendeesValues);
- DatabaseUtils.cursorStringToContentValues(attendeesCursor,
- Attendees.ATTENDEE_EMAIL,
- attendeesValues);
- DatabaseUtils.cursorIntToContentValues(attendeesCursor,
- Attendees.ATTENDEE_STATUS,
- attendeesValues);
- DatabaseUtils.cursorIntToContentValues(attendeesCursor,
- Attendees.ATTENDEE_TYPE,
- attendeesValues);
- DatabaseUtils.cursorIntToContentValues(attendeesCursor,
- Attendees.ATTENDEE_RELATIONSHIP,
- attendeesValues);
- attendeesValues.put(Attendees.EVENT_ID, rowId);
- mAttendeesInserter.insert(attendeesValues);
- }
- } finally {
- if (attendeesCursor != null) {
- attendeesCursor.close();
- }
- }
- }
-
- private void insertRemindersIfNecessary(ContentProvider diffs,
- long diffsRowId,
- long rowId,
- SQLiteDatabase db) {
- // insert reminders, if necessary.
- Integer hasAlarm = mValues.getAsInteger(Events.HAS_ALARM);
- if (hasAlarm != null && hasAlarm.intValue() == 1) {
- // query reminders in diffs
- Cursor reminderCursor =
- diffs.query(Reminders.CONTENT_URI, null,
- "event_id=" + diffsRowId, null, null);
- ContentValues reminderValues = new ContentValues();
- try {
- while (reminderCursor.moveToNext()) {
- reminderValues.clear();
- DatabaseUtils.cursorIntToContentValues(reminderCursor,
- Reminders.METHOD,
- reminderValues);
- DatabaseUtils.cursorIntToContentValues(reminderCursor,
- Reminders.MINUTES,
- reminderValues);
- reminderValues.put(Reminders.EVENT_ID, rowId);
- mRemindersInserter.insert(reminderValues);
- }
- } finally {
- if (reminderCursor != null) {
- reminderCursor.close();
- }
- }
- }
- }
-
- private void insertExtendedPropertiesIfNecessary(ContentProvider diffs,
- long diffsRowId,
- long rowId,
- SQLiteDatabase db) {
- // insert extended properties, if necessary.
- Integer hasExtendedProperties = mValues.getAsInteger(Events.HAS_EXTENDED_PROPERTIES);
- if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
- // query reminders in diffs
- Cursor extendedPropertiesCursor =
- diffs.query(Calendar.ExtendedProperties.CONTENT_URI, null,
- "event_id=" + diffsRowId, null, null);
- ContentValues extendedPropertiesValues = new ContentValues();
- try {
- while (extendedPropertiesCursor.moveToNext()) {
- extendedPropertiesValues.clear();
- DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
- Calendar.ExtendedProperties.NAME, extendedPropertiesValues);
- DatabaseUtils.cursorStringToContentValues(extendedPropertiesCursor,
- Calendar.ExtendedProperties.VALUE, extendedPropertiesValues);
- extendedPropertiesValues.put(ExtendedProperties.EVENT_ID, rowId);
- mExtendedPropertiesInserter.insert(extendedPropertiesValues);
- }
- } finally {
- if (extendedPropertiesCursor != null) {
- extendedPropertiesCursor.close();
- }
- }
- }
- }
-
- @Override
- public void updateRow(long localId, ContentProvider diffs,
- Cursor diffsCursor) {
- rowToContentValues(diffsCursor, mValues);
- final SQLiteDatabase db = getDatabase();
- updateBusyBitsLocked(localId, mValues);
- int numRows = db.update(mTable, mValues, "_id=" + localId, null /* selectionArgs */);
-
- if (numRows <= 0) {
- Log.e(TAG, "Unable to update calendar db: " + mValues);
- return;
- }
-
- long diffsRowId = diffsCursor.getLong(
- diffsCursor.getColumnIndex(Events._ID));
- // TODO: only update the attendees, reminders, and extended properties if they have
- // changed?
- // delete the existing attendees, reminders, and extended properties
- db.delete(sAttendeesTable, "event_id=" + localId, null /* selectionArgs */);
- db.delete(sRemindersTable, "event_id=" + localId, null /* selectionArgs */);
- db.delete(sExtendedPropertiesTable, "event_id=" + localId,
- null /* selectionArgs */);
-
- // process attendees sent by the server.
- insertAttendees(diffs, diffsRowId, localId, db);
- // process reminders sent by the server.
- insertRemindersIfNecessary(diffs, diffsRowId, localId, db);
-
- // process extended properties sent by the server.
- insertExtendedPropertiesIfNecessary(diffs, diffsRowId, localId, db);
-
- updateEventRawTimesLocked(localId, mValues);
- updateInstancesLocked(mValues, localId, false /* not a new event */, db);
-
- // Update the _SYNC_DIRTY flag of the event. We have to do this
- // after updating since the update of the reminders and extended properties
- // methods will fire a sql trigger that will cause this flag to
- // be set.
- clearSyncDirtyFlag(db, localId);
- }
-
- @Override
- public void resolveRow(long localId, String syncId,
- ContentProvider diffs, Cursor diffsCursor) {
- // server wins
- updateRow(localId, diffs, diffsCursor);
- }
-
- @Override
- public void deleteRow(Cursor localCursor) {
- long localId = localCursor.getLong(localCursor.getColumnIndexOrThrow(Events._ID));
- deleteBusyBitsLocked(localId);
-
- // we have to read this row from the DB since the projection that is used
- // by cursor doesn't necessarily contain the columns we need
- Cursor c = getDatabase().query(sEventsTable,
- new String[]{Events.RRULE, Events.RDATE, Events.ORIGINAL_EVENT},
- "_id=" + localId, null, null, null, null);
- try {
- c.moveToNext();
- // If this was a recurring event or a recurrence exception, then
- // force a recalculation of the instances.
- // We can get a tombstoned recurrence exception
- // that doesn't have a rrule, rdate, or originalEvent, and the
- // check below wouldn't catch that. However, in practice we also
- // get a different event with a rrule in that case, so the
- // instances get cleared by that rule.
- // This should be re-evaluated when calendar supports gd:deleted.
- String rrule = c.getString(c.getColumnIndexOrThrow(Events.RRULE));
- String rdate = c.getString(c.getColumnIndexOrThrow(Events.RDATE));
- String origEvent = c.getString(c.getColumnIndexOrThrow(Events.ORIGINAL_EVENT));
- if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
- || !TextUtils.isEmpty(origEvent)) {
- mMetaData.clearInstanceRange();
- }
- } finally {
- c.close();
- }
- super.deleteRow(localCursor);
- }
-
- private void rowToContentValues(Cursor diffsCursor, ContentValues values) {
- values.clear();
-
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ID, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_TIME, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor,
- Events._SYNC_ACCOUNT_TYPE, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DESCRIPTION, values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.STATUS, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.SELF_ATTENDEE_STATUS,
- values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.COMMENTS_URI, values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTSTART, values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.DTEND, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_TIMEZONE, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.DURATION, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.ALL_DAY, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.VISIBILITY, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.TRANSPARENCY, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ALARM, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_EXTENDED_PROPERTIES,
- values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.RRULE, values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_EVENT, values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.ORIGINAL_INSTANCE_TIME,
- values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORIGINAL_ALL_DAY,
- values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.LAST_DATE, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.HAS_ATTENDEE_DATA, values);
- DatabaseUtils.cursorLongToContentValues(diffsCursor, Events.CALENDAR_ID, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_INVITE_OTHERS,
- values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_MODIFY, values);
- DatabaseUtils.cursorIntToContentValues(diffsCursor, Events.GUESTS_CAN_SEE_GUESTS,
- values);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.ORGANIZER, values);
- }
- }
-
- private static final int EVENTS = 1;
- private static final int EVENTS_ID = 2;
- private static final int INSTANCES = 3;
- private static final int DELETED_EVENTS = 4;
- private static final int CALENDARS = 5;
- private static final int CALENDARS_ID = 6;
- private static final int ATTENDEES = 7;
- private static final int ATTENDEES_ID = 8;
- private static final int REMINDERS = 9;
- private static final int REMINDERS_ID = 10;
- private static final int EXTENDED_PROPERTIES = 11;
- private static final int EXTENDED_PROPERTIES_ID = 12;
- private static final int CALENDAR_ALERTS = 13;
- private static final int CALENDAR_ALERTS_ID = 14;
- private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
- private static final int BUSYBITS = 16;
- private static final int INSTANCES_BY_DAY = 17;
-
- private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- private static final HashMap<String, String> sInstancesProjectionMap;
- private static final HashMap<String, String> sEventsProjectionMap;
- private static final HashMap<String, String> sAttendeesProjectionMap;
- private static final HashMap<String, String> sRemindersProjectionMap;
- private static final HashMap<String, String> sCalendarAlertsProjectionMap;
- private static final HashMap<String, String> sBusyBitsProjectionMap;
-
- static {
- sURLMatcher.addURI("calendar", "instances/when/*/*", INSTANCES);
- sURLMatcher.addURI("calendar", "instances/whenbyday/*/*", INSTANCES_BY_DAY);
- sURLMatcher.addURI("calendar", "events", EVENTS);
- sURLMatcher.addURI("calendar", "events/#", EVENTS_ID);
- sURLMatcher.addURI("calendar", "calendars", CALENDARS);
- sURLMatcher.addURI("calendar", "calendars/#", CALENDARS_ID);
- sURLMatcher.addURI("calendar", "deleted_events", DELETED_EVENTS);
- sURLMatcher.addURI("calendar", "attendees", ATTENDEES);
- sURLMatcher.addURI("calendar", "attendees/#", ATTENDEES_ID);
- sURLMatcher.addURI("calendar", "reminders", REMINDERS);
- sURLMatcher.addURI("calendar", "reminders/#", REMINDERS_ID);
- sURLMatcher.addURI("calendar", "extendedproperties", EXTENDED_PROPERTIES);
- sURLMatcher.addURI("calendar", "extendedproperties/#", EXTENDED_PROPERTIES_ID);
- sURLMatcher.addURI("calendar", "calendar_alerts", CALENDAR_ALERTS);
- sURLMatcher.addURI("calendar", "calendar_alerts/#", CALENDAR_ALERTS_ID);
- sURLMatcher.addURI("calendar", "calendar_alerts/by_instance", CALENDAR_ALERTS_BY_INSTANCE);
- sURLMatcher.addURI("calendar", "busybits/when/*/*", BUSYBITS);
-
-
- sEventsProjectionMap = new HashMap<String, String>();
- // Events columns
- sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
- sEventsProjectionMap.put(Events.TITLE, "title");
- sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
- sEventsProjectionMap.put(Events.DESCRIPTION, "description");
- sEventsProjectionMap.put(Events.STATUS, "eventStatus");
- sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
- sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
- sEventsProjectionMap.put(Events.DTSTART, "dtstart");
- sEventsProjectionMap.put(Events.DTEND, "dtend");
- sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
- sEventsProjectionMap.put(Events.DURATION, "duration");
- sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
- sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
- sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
- sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
- sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
- sEventsProjectionMap.put(Events.RRULE, "rrule");
- sEventsProjectionMap.put(Events.RDATE, "rdate");
- sEventsProjectionMap.put(Events.EXRULE, "exrule");
- sEventsProjectionMap.put(Events.EXDATE, "exdate");
- sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
- sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
- sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
- sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
- sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
- sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
- sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
- sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
- sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
- sEventsProjectionMap.put(Events.ORGANIZER, "organizer");
-
- // Calendar columns
- sEventsProjectionMap.put(Events.COLOR, "color");
- sEventsProjectionMap.put(Events.ACCESS_LEVEL, "access_level");
- sEventsProjectionMap.put(Events.SELECTED, "selected");
- sEventsProjectionMap.put(Calendars.URL, "url");
- sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
- sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount");
-
- // Put the shared items into the Instances projection map
- sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
- sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
- sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
- sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
-
- sEventsProjectionMap.put(Events._ID, "Events._id AS _id");
- sEventsProjectionMap.put(Events._SYNC_ID, "Events._sync_id AS _sync_id");
- sEventsProjectionMap.put(Events._SYNC_VERSION, "Events._sync_version AS _sync_version");
- sEventsProjectionMap.put(Events._SYNC_TIME, "Events._sync_time AS _sync_time");
- sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id");
- sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty");
- sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account");
- sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
- "Events._sync_account_type AS _sync_account_type");
-
- // Instances columns
- sInstancesProjectionMap.put(Instances.BEGIN, "begin");
- sInstancesProjectionMap.put(Instances.END, "end");
- sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
- sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
- sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
- sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
- sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
- sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
-
- // BusyBits columns
- sBusyBitsProjectionMap = new HashMap<String, String>();
- sBusyBitsProjectionMap.put(BusyBits.DAY, "day");
- sBusyBitsProjectionMap.put(BusyBits.BUSYBITS, "busyBits");
- sBusyBitsProjectionMap.put(BusyBits.ALL_DAY_COUNT, "allDayCount");
-
- // Attendees columns
- sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
- sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
- sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
- sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
- sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
- sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
- sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
-
- // Reminders columns
- sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
- sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
- sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
- sRemindersProjectionMap.put(Reminders.METHOD, "method");
-
- // CalendarAlerts columns
- sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
- sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
- sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
- sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
- sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
- sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
- sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
- }
-
- /**
- * An implementation of EntityIterator that builds the Entity for a calendar event.
- */
- private static class CalendarEntityIterator implements EntityIterator {
- private final Cursor mEntityCursor;
- private volatile boolean mIsClosed;
- private final SQLiteDatabase mDb;
-
- private static final String[] EVENTS_PROJECTION = new String[]{
- Calendar.Events._ID,
- Calendar.Events.HTML_URI,
- Calendar.Events.TITLE,
- Calendar.Events.DESCRIPTION,
- Calendar.Events.EVENT_LOCATION,
- Calendar.Events.STATUS,
- Calendar.Events.SELF_ATTENDEE_STATUS,
- Calendar.Events.COMMENTS_URI,
- Calendar.Events.DTSTART,
- Calendar.Events.DTEND,
- Calendar.Events.DURATION,
- Calendar.Events.EVENT_TIMEZONE,
- Calendar.Events.ALL_DAY,
- Calendar.Events.VISIBILITY,
- Calendar.Events.TRANSPARENCY,
- Calendar.Events.HAS_ALARM,
- Calendar.Events.HAS_EXTENDED_PROPERTIES,
- Calendar.Events.RRULE,
- Calendar.Events.RDATE,
- Calendar.Events.EXRULE,
- Calendar.Events.EXDATE,
- Calendar.Events.ORIGINAL_EVENT,
- Calendar.Events.ORIGINAL_INSTANCE_TIME,
- Calendar.Events.ORIGINAL_ALL_DAY,
- Calendar.Events.LAST_DATE,
- Calendar.Events.HAS_ATTENDEE_DATA,
- Calendar.Events.CALENDAR_ID,
- Calendar.Events.GUESTS_CAN_INVITE_OTHERS,
- Calendar.Events.GUESTS_CAN_MODIFY,
- Calendar.Events.GUESTS_CAN_SEE_GUESTS,
- Calendar.Events.ORGANIZER,
- };
- private static final int COLUMN_ID = 0;
- private static final int COLUMN_HTML_URI = 1;
- private static final int COLUMN_TITLE = 2;
- private static final int COLUMN_DESCRIPTION = 3;
- private static final int COLUMN_EVENT_LOCATION = 4;
- private static final int COLUMN_STATUS = 5;
- private static final int COLUMN_SELF_ATTENDEE_STATUS = 6;
- private static final int COLUMN_COMMENTS_URI = 7;
- private static final int COLUMN_DTSTART = 8;
- private static final int COLUMN_DTEND = 9;
- private static final int COLUMN_DURATION = 10;
- private static final int COLUMN_EVENT_TIMEZONE = 11;
- private static final int COLUMN_ALL_DAY = 12;
- private static final int COLUMN_VISIBILITY = 13;
- private static final int COLUMN_TRANSPARENCY = 14;
- private static final int COLUMN_HAS_ALARM = 15;
- private static final int COLUMN_HAS_EXTENDED_PROPERTIES = 16;
- private static final int COLUMN_RRULE = 17;
- private static final int COLUMN_RDATE = 18;
- private static final int COLUMN_EXRULE = 19;
- private static final int COLUMN_EXDATE = 20;
- private static final int COLUMN_ORIGINAL_EVENT = 21;
- private static final int COLUMN_ORIGINAL_INSTANCE_TIME = 22;
- private static final int COLUMN_ORIGINAL_ALL_DAY = 23;
- private static final int COLUMN_LAST_DATE = 24;
- private static final int COLUMN_HAS_ATTENDEE_DATA = 25;
- private static final int COLUMN_CALENDAR_ID = 26;
- private static final int COLUMN_GUESTS_CAN_INVITE_OTHERS = 27;
- private static final int COLUMN_GUESTS_CAN_MODIFY = 28;
- private static final int COLUMN_GUESTS_CAN_SEE_GUESTS = 29;
- private static final int COLUMN_ORGANIZER = 30;
-
- private static final String[] REMINDERS_PROJECTION = new String[] {
- Calendar.Reminders.MINUTES,
- Calendar.Reminders.METHOD,
- };
- private static final int COLUMN_MINUTES = 0;
- private static final int COLUMN_METHOD = 1;
-
- private static final String[] ATTENDEES_PROJECTION = new String[] {
- Calendar.Attendees.ATTENDEE_NAME,
- Calendar.Attendees.ATTENDEE_EMAIL,
- Calendar.Attendees.ATTENDEE_RELATIONSHIP,
- Calendar.Attendees.ATTENDEE_TYPE,
- Calendar.Attendees.ATTENDEE_STATUS,
- };
- private static final int COLUMN_ATTENDEE_NAME = 0;
- private static final int COLUMN_ATTENDEE_EMAIL = 1;
- private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2;
- private static final int COLUMN_ATTENDEE_TYPE = 3;
- private static final int COLUMN_ATTENDEE_STATUS = 4;
- private static final String[] EXTENDED_PROJECTION = new String[] {
- Calendar.ExtendedProperties.NAME,
- Calendar.ExtendedProperties.VALUE,
- };
- private static final int COLUMN_NAME = 0;
- private static final int COLUMN_VALUE = 1;
-
- public CalendarEntityIterator(CalendarProvider provider, String eventIdString, Uri uri,
- String selection, String[] selectionArgs, String sortOrder) {
- mIsClosed = false;
- mDb = provider.mOpenHelper.getReadableDatabase();
- final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables(sEventsTable);
- if (eventIdString != null) {
- qb.appendWhere(Calendar.Events._ID + "=" + eventIdString);
- }
- mEntityCursor = qb.query(mDb, EVENTS_PROJECTION, selection, selectionArgs,
- null, null, sortOrder);
- mEntityCursor.moveToFirst();
- }
-
- public void close() {
- if (mIsClosed) {
- throw new IllegalStateException("closing when already closed");
- }
- mIsClosed = true;
- mEntityCursor.close();
- }
-
- public boolean hasNext() throws RemoteException {
-
- if (mIsClosed) {
- throw new IllegalStateException("calling hasNext() when the iterator is closed");
- }
-
- return !mEntityCursor.isAfterLast();
- }
-
- public void reset() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling next() when the iterator is closed");
- }
- mEntityCursor.moveToFirst();
- }
-
- public Entity next() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling next() when the iterator is closed");
- }
- if (!hasNext()) {
- throw new IllegalStateException("you may only call next() if hasNext() is true");
- }
-
- final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
- final long eventId = c.getLong(COLUMN_ID);
-
- // we expect the cursor is already at the row we need to read from
- ContentValues entityValues = new ContentValues();
- entityValues.put(Calendar.Events._ID, eventId);
- entityValues.put(Calendar.Events.CALENDAR_ID, c.getInt(COLUMN_CALENDAR_ID));
- entityValues.put(Calendar.Events.HTML_URI, c.getString(COLUMN_HTML_URI));
- entityValues.put(Calendar.Events.TITLE, c.getString(COLUMN_TITLE));
- entityValues.put(Calendar.Events.DESCRIPTION, c.getString(COLUMN_DESCRIPTION));
- entityValues.put(Calendar.Events.EVENT_LOCATION, c.getString(COLUMN_EVENT_LOCATION));
- entityValues.put(Calendar.Events.STATUS, c.getInt(COLUMN_STATUS));
- entityValues.put(Calendar.Events.SELF_ATTENDEE_STATUS,
- c.getInt(COLUMN_SELF_ATTENDEE_STATUS));
- entityValues.put(Calendar.Events.COMMENTS_URI, c.getString(COLUMN_COMMENTS_URI));
- entityValues.put(Calendar.Events.DTSTART, c.getLong(COLUMN_DTSTART));
- entityValues.put(Calendar.Events.DTEND, c.getLong(COLUMN_DTEND));
- entityValues.put(Calendar.Events.DURATION, c.getString(COLUMN_DURATION));
- entityValues.put(Calendar.Events.EVENT_TIMEZONE, c.getString(COLUMN_EVENT_TIMEZONE));
- entityValues.put(Calendar.Events.ALL_DAY, c.getString(COLUMN_ALL_DAY));
- entityValues.put(Calendar.Events.VISIBILITY, c.getInt(COLUMN_VISIBILITY));
- entityValues.put(Calendar.Events.TRANSPARENCY, c.getInt(COLUMN_TRANSPARENCY));
- entityValues.put(Calendar.Events.HAS_ALARM, c.getString(COLUMN_HAS_ALARM));
- entityValues.put(Calendar.Events.HAS_EXTENDED_PROPERTIES,
- c.getString(COLUMN_HAS_EXTENDED_PROPERTIES));
- entityValues.put(Calendar.Events.RRULE, c.getString(COLUMN_RRULE));
- entityValues.put(Calendar.Events.RDATE, c.getString(COLUMN_RDATE));
- entityValues.put(Calendar.Events.EXRULE, c.getString(COLUMN_EXRULE));
- entityValues.put(Calendar.Events.EXDATE, c.getString(COLUMN_EXDATE));
- entityValues.put(Calendar.Events.ORIGINAL_EVENT, c.getString(COLUMN_ORIGINAL_EVENT));
- entityValues.put(Calendar.Events.ORIGINAL_INSTANCE_TIME,
- c.getLong(COLUMN_ORIGINAL_INSTANCE_TIME));
- entityValues.put(Calendar.Events.ORIGINAL_ALL_DAY, c.getInt(COLUMN_ORIGINAL_ALL_DAY));
- entityValues.put(Calendar.Events.LAST_DATE, c.getLong(COLUMN_LAST_DATE));
- entityValues.put(Calendar.Events.HAS_ATTENDEE_DATA,
- c.getInt(COLUMN_HAS_ATTENDEE_DATA));
- entityValues.put(Calendar.Events.GUESTS_CAN_INVITE_OTHERS,
- c.getInt(COLUMN_GUESTS_CAN_INVITE_OTHERS));
- entityValues.put(Calendar.Events.GUESTS_CAN_MODIFY,
- c.getInt(COLUMN_GUESTS_CAN_MODIFY));
- entityValues.put(Calendar.Events.GUESTS_CAN_SEE_GUESTS,
- c.getInt(COLUMN_GUESTS_CAN_SEE_GUESTS));
- entityValues.put(Calendar.Events.ORGANIZER, c.getString(COLUMN_ORGANIZER));
-
- Entity entity = new Entity(entityValues);
- Cursor cursor = null;
- try {
- cursor = mDb.query(sRemindersTable, REMINDERS_PROJECTION, "event_id=" + eventId,
- null, null, null, null);
- while (cursor.moveToNext()) {
- ContentValues reminderValues = new ContentValues();
- reminderValues.put(Calendar.Reminders.MINUTES, cursor.getInt(COLUMN_MINUTES));
- reminderValues.put(Calendar.Reminders.METHOD, cursor.getInt(COLUMN_METHOD));
- entity.addSubValue(Calendar.Reminders.CONTENT_URI, reminderValues);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- cursor = null;
- try {
- cursor = mDb.query(sAttendeesTable, ATTENDEES_PROJECTION, "event_id=" + eventId,
- null, null, null, null);
- while (cursor.moveToNext()) {
- ContentValues attendeeValues = new ContentValues();
- attendeeValues.put(Calendar.Attendees.ATTENDEE_NAME,
- cursor.getString(COLUMN_ATTENDEE_NAME));
- attendeeValues.put(Calendar.Attendees.ATTENDEE_EMAIL,
- cursor.getString(COLUMN_ATTENDEE_EMAIL));
- attendeeValues.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
- cursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP));
- attendeeValues.put(Calendar.Attendees.ATTENDEE_TYPE,
- cursor.getInt(COLUMN_ATTENDEE_TYPE));
- attendeeValues.put(Calendar.Attendees.ATTENDEE_STATUS,
- cursor.getInt(COLUMN_ATTENDEE_STATUS));
- entity.addSubValue(Calendar.Attendees.CONTENT_URI, attendeeValues);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- cursor = null;
- try {
- cursor = mDb.query(sExtendedPropertiesTable, EXTENDED_PROJECTION,
- "event_id=" + eventId, null, null, null, null);
- while (cursor.moveToNext()) {
- ContentValues extendedValues = new ContentValues();
- extendedValues.put(Calendar.ExtendedProperties.NAME, c.getString(COLUMN_NAME));
- extendedValues.put(Calendar.ExtendedProperties.VALUE,
- c.getString(COLUMN_VALUE));
- entity.addSubValue(Calendar.ExtendedProperties.CONTENT_URI, extendedValues);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- mEntityCursor.moveToNext();
- // add the data to the contact
- return entity;
- }
- }
-
- @Override
- public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
- String sortOrder) {
- final int match = sURLMatcher.match(uri);
- switch (match) {
- case EVENTS:
- case EVENTS_ID:
- String calendarId = null;
- if (match == EVENTS_ID) {
- calendarId = uri.getPathSegments().get(1);
- }
-
- return new CalendarEntityIterator(this, calendarId,
- uri, selection, selectionArgs, sortOrder);
- default:
- throw new UnsupportedOperationException("Unknown uri: " + uri);
- }
- }
-
- @Override
- public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
- throws OperationApplicationException {
-
- final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- db.beginTransaction();
- try {
- ContentProviderResult[] results = super.applyBatch(operations);
- db.setTransactionSuccessful();
- return results;
- } finally {
- db.endTransaction();
- }
- }
-}
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
new file mode 100644
index 0000000..2a5965f
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -0,0 +1,3208 @@
+/*
+**
+** Copyright 2006, 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,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.android.providers.calendar;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Debug;
+import android.os.Process;
+import android.pim.DateException;
+import android.pim.RecurrenceSet;
+import android.provider.BaseColumns;
+import android.provider.Calendar;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.CalendarAlerts;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.Instances;
+import android.provider.Calendar.Reminders;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.text.format.DateUtils;
+import android.util.Config;
+import android.util.Log;
+import android.util.TimeFormatException;
+import android.util.TimeUtils;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Calendar content provider. The contract between this provider and applications
+ * is defined in {@link android.provider.Calendar}.
+ */
+public class CalendarProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
+
+ private static final String TAG = "CalendarProvider2";
+
+ private static final boolean PROFILE = false;
+ private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = true;
+
+ private static final String INVALID_CALENDARALERTS_SELECTOR =
+ "_id IN (SELECT ca._id FROM CalendarAlerts AS ca"
+ + " LEFT OUTER JOIN Instances USING (event_id, begin, end)"
+ + " LEFT OUTER JOIN Reminders AS r ON"
+ + " (ca.event_id=r.event_id AND ca.minutes=r.minutes)"
+ + " WHERE Instances.begin ISNULL OR ca.alarmTime<?"
+ + " OR (r.minutes ISNULL AND ca.minutes<>0))";
+
+ private static final String[] ID_ONLY_PROJECTION =
+ new String[] {Events._ID};
+
+ private static final String[] EVENTS_PROJECTION = new String[] {
+ Events._SYNC_ID,
+ Events.RRULE,
+ Events.RDATE,
+ Events.ORIGINAL_EVENT,
+ };
+ private static final int EVENTS_SYNC_ID_INDEX = 0;
+ private static final int EVENTS_RRULE_INDEX = 1;
+ private static final int EVENTS_RDATE_INDEX = 2;
+ private static final int EVENTS_ORIGINAL_EVENT_INDEX = 3;
+
+ private static final String[] ID_PROJECTION = new String[] {
+ Attendees._ID,
+ Attendees.EVENT_ID, // Assume these are the same for each table
+ };
+ private static final int ID_INDEX = 0;
+ private static final int EVENT_ID_INDEX = 1;
+
+ /**
+ * The cached copy of the CalendarMetaData database table.
+ * Make this "package private" instead of "private" so that test code
+ * can access it.
+ */
+ MetaData mMetaData;
+ CalendarCache mCalendarCache;
+
+ private CalendarDatabaseHelper mDbHelper;
+
+ private static final Uri SYNCSTATE_CONTENT_URI =
+ Uri.parse("content://syncstate/state");
+ //
+ // SCHEDULE_ALARM_URI runs scheduleNextAlarm(false)
+ // SCHEDULE_ALARM_REMOVE_URI runs scheduleNextAlarm(true)
+ // TODO: use a service to schedule alarms rather than private URI
+ /* package */ static final String SCHEDULE_ALARM_PATH = "schedule_alarms";
+ /* package */ static final String SCHEDULE_ALARM_REMOVE_PATH = "schedule_alarms_remove";
+ /* package */ static final Uri SCHEDULE_ALARM_URI =
+ Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_PATH);
+ /* package */ static final Uri SCHEDULE_ALARM_REMOVE_URI =
+ Uri.withAppendedPath(Calendar.CONTENT_URI, SCHEDULE_ALARM_REMOVE_PATH);
+
+ // To determine if a recurrence exception originally overlapped the
+ // window, we need to assume a maximum duration, since we only know
+ // the original start time.
+ private static final int MAX_ASSUMED_DURATION = 7*24*60*60*1000;
+
+ public static final class TimeRange {
+ public long begin;
+ public long end;
+ public boolean allDay;
+ }
+
+ public static final class InstancesRange {
+ public long begin;
+ public long end;
+
+ public InstancesRange(long begin, long end) {
+ this.begin = begin;
+ this.end = end;
+ }
+ }
+
+ public static final class InstancesList
+ extends ArrayList<ContentValues> {
+ }
+
+ public static final class EventInstancesMap
+ extends HashMap<String, InstancesList> {
+ public void add(String syncId, ContentValues values) {
+ InstancesList instances = get(syncId);
+ if (instances == null) {
+ instances = new InstancesList();
+ put(syncId, instances);
+ }
+ instances.add(values);
+ }
+ }
+
+ // A thread that runs in the background and schedules the next
+ // calendar event alarm.
+ private class AlarmScheduler extends Thread {
+ boolean mRemoveAlarms;
+
+ public AlarmScheduler(boolean removeAlarms) {
+ mRemoveAlarms = removeAlarms;
+ }
+
+ public void run() {
+ try {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ runScheduleNextAlarm(mRemoveAlarms);
+ } catch (SQLException e) {
+ Log.e(TAG, "runScheduleNextAlarm() failed", e);
+ }
+ }
+ }
+
+ /**
+ * We search backward in time for event reminders that we may have missed
+ * and schedule them if the event has not yet expired. The amount in
+ * the past to search backwards is controlled by this constant. It
+ * should be at least a few minutes to allow for an event that was
+ * recently created on the web to make its way to the phone. Two hours
+ * might seem like overkill, but it is useful in the case where the user
+ * just crossed into a new timezone and might have just missed an alarm.
+ */
+ private static final long SCHEDULE_ALARM_SLACK = 2 * DateUtils.HOUR_IN_MILLIS;
+
+ /**
+ * Alarms older than this threshold will be deleted from the CalendarAlerts
+ * table. This should be at least a day because if the timezone is
+ * wrong and the user corrects it we might delete good alarms that
+ * appear to be old because the device time was incorrectly in the future.
+ * This threshold must also be larger than SCHEDULE_ALARM_SLACK. We add
+ * the SCHEDULE_ALARM_SLACK to ensure this.
+ *
+ * To make it easier to find and debug problems with missed reminders,
+ * set this to something greater than a day.
+ */
+ private static final long CLEAR_OLD_ALARM_THRESHOLD =
+ 7 * DateUtils.DAY_IN_MILLIS + SCHEDULE_ALARM_SLACK;
+
+ // A lock for synchronizing access to fields that are shared
+ // with the AlarmScheduler thread.
+ private Object mAlarmLock = new Object();
+
+ // Make sure we load at least two months worth of data.
+ // Client apps can load more data in a background thread.
+ private static final long MINIMUM_EXPANSION_SPAN =
+ 2L * 31 * 24 * 60 * 60 * 1000;
+
+ private static final String[] sCalendarsIdProjection = new String[] { Calendars._ID };
+ private static final int CALENDARS_INDEX_ID = 0;
+
+ // Allocate the string constant once here instead of on the heap
+ private static final String CALENDAR_ID_SELECTION = "calendar_id=?";
+
+ private static final String[] sInstancesProjection =
+ new String[] { Instances.START_DAY, Instances.END_DAY,
+ Instances.START_MINUTE, Instances.END_MINUTE, Instances.ALL_DAY };
+
+ private static final int INSTANCES_INDEX_START_DAY = 0;
+ private static final int INSTANCES_INDEX_END_DAY = 1;
+ private static final int INSTANCES_INDEX_START_MINUTE = 2;
+ private static final int INSTANCES_INDEX_END_MINUTE = 3;
+ private static final int INSTANCES_INDEX_ALL_DAY = 4;
+
+ private CalendarClient mCalendarClient = null;
+
+ private AlarmManager mAlarmManager;
+
+ private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
+
+ /**
+ * Listens for timezone changes and disk-no-longer-full events
+ */
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onReceive() " + action);
+ }
+ if (Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
+ updateTimezoneDependentFields();
+ scheduleNextAlarm(false /* do not remove alarms */);
+ } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+ // Try to clean up if things were screwy due to a full disk
+ updateTimezoneDependentFields();
+ scheduleNextAlarm(false /* do not remove alarms */);
+ } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
+ scheduleNextAlarm(false /* do not remove alarms */);
+ }
+ }
+ };
+
+ /**
+ * Columns from the EventsRawTimes table
+ */
+ public interface EventsRawTimesColumns
+ {
+ /**
+ * The corresponding event id
+ * <P>Type: INTEGER (long)</P>
+ */
+ public static final String EVENT_ID = "event_id";
+
+ /**
+ * The RFC2445 compliant time the event starts
+ * <P>Type: TEXT</P>
+ */
+ public static final String DTSTART_2445 = "dtstart2445";
+
+ /**
+ * The RFC2445 compliant time the event ends
+ * <P>Type: TEXT</P>
+ */
+ public static final String DTEND_2445 = "dtend2445";
+
+ /**
+ * The RFC2445 compliant original instance time of the recurring event for which this
+ * event is an exception.
+ * <P>Type: TEXT</P>
+ */
+ public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445";
+
+ /**
+ * The RFC2445 compliant last date this event repeats on, or NULL if it never ends
+ * <P>Type: TEXT</P>
+ */
+ public static final String LAST_DATE_2445 = "lastDate2445";
+ }
+
+ protected void verifyAccounts() {
+ AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
+ onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
+ }
+
+ /* Visible for testing */
+ @Override
+ protected CalendarDatabaseHelper getDatabaseHelper(final Context context) {
+ return CalendarDatabaseHelper.getInstance(context);
+ }
+
+ @Override
+ public boolean onCreate() {
+ super.onCreate();
+ mDbHelper = (CalendarDatabaseHelper)getDatabaseHelper();
+
+ verifyAccounts();
+
+ // Register for Intent broadcasts
+ IntentFilter filter = new IntentFilter();
+
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ filter.addAction(Intent.ACTION_TIME_CHANGED);
+ final Context c = getContext();
+
+ // We don't ever unregister this because this thread always wants
+ // to receive notifications, even in the background. And if this
+ // thread is killed then the whole process will be killed and the
+ // memory resources will be reclaimed.
+ c.registerReceiver(mIntentReceiver, filter);
+
+ mMetaData = new MetaData(mDbHelper);
+ mCalendarCache = new CalendarCache(mDbHelper);
+
+ updateTimezoneDependentFields();
+
+ return true;
+ }
+
+ /**
+ * This creates a background thread to check the timezone and update
+ * the timezone dependent fields in the Instances table if the timezone
+ * has changes.
+ */
+ protected void updateTimezoneDependentFields() {
+ Thread thread = new TimezoneCheckerThread();
+ thread.start();
+ }
+
+ private class TimezoneCheckerThread extends Thread {
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ try {
+ doUpdateTimezoneDependentFields();
+ } catch (SQLException e) {
+ Log.e(TAG, "doUpdateTimezoneDependentFields() failed", e);
+ try {
+ // Clear at least the in-memory data (and if possible the
+ // database fields) to force a re-computation of Instances.
+ mMetaData.clearInstanceRange();
+ } catch (SQLException e2) {
+ Log.e(TAG, "clearInstanceRange() also failed: " + e2);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method runs in a background thread. If the timezone has changed
+ * then the Instances table will be regenerated.
+ */
+ private void doUpdateTimezoneDependentFields() {
+ if (! isSameTimezoneDatabaseVersion()) {
+ doProcessEventRawTimes(null /* default current timezone*/,
+ TimeUtils.getTimeZoneDatabaseVersion());
+ }
+ if (isSameTimezone()) {
+ // Even if the timezone hasn't changed, check for missed alarms.
+ // This code executes when the CalendarProvider2 is created and
+ // helps to catch missed alarms when the Calendar process is
+ // killed (because of low-memory conditions) and then restarted.
+ rescheduleMissedAlarms();
+ return;
+ }
+ regenerateInstancesTable();
+ }
+
+ protected void doProcessEventRawTimes(String timezone, String timeZoneDatabaseVersion) {
+ mDb = mDbHelper.getWritableDatabase();
+ if (mDb == null) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Cannot update Events table from EventsRawTimes table");
+ }
+ return;
+ }
+ mDb.beginTransaction();
+ try {
+ updateEventsStartEndFromEventRawTimesLocked(timezone);
+ updateTimezoneDatabaseVersion(timeZoneDatabaseVersion);
+ cleanInstancesTable();
+ regenerateInstancesTable();
+
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ private void updateEventsStartEndFromEventRawTimesLocked(String timezone) {
+ Cursor cursor = mDb.query("EventsRawTimes",
+ new String[] { EventsRawTimesColumns.EVENT_ID,
+ EventsRawTimesColumns.DTSTART_2445,
+ EventsRawTimesColumns.DTEND_2445} /* projection */,
+ null /* selection */,
+ null /* selection args */,
+ null /* group by */,
+ null /* having */,
+ null /* order by */
+ );
+ try {
+ while (cursor.moveToNext()) {
+ long eventId = cursor.getLong(0);
+ String dtStart2445 = cursor.getString(1);
+ String dtEnd2445 = cursor.getString(2);
+ updateEventsStartEndLocked(eventId,
+ timezone,
+ dtStart2445,
+ dtEnd2445);
+ }
+ } finally {
+ cursor.close();
+ cursor = null;
+ }
+ }
+
+ private long get2445ToMillis(String timezone, String dt2445) {
+ if (null == dt2445) {
+ Log.v( TAG, "Cannot parse null RFC2445 date");
+ return 0;
+ }
+ Time time = (timezone != null) ? new Time(timezone) : new Time();
+ try {
+ time.parse(dt2445);
+ } catch (TimeFormatException e) {
+ Log.v( TAG, "Cannot parse RFC2445 date " + dt2445);
+ return 0;
+ }
+ return time.toMillis(true /* ignore DST */);
+ }
+
+ private void updateEventsStartEndLocked(long eventId,
+ String timezone, String dtStart2445, String dtEnd2445) {
+
+ ContentValues values = new ContentValues();
+ values.put("dtstart", get2445ToMillis(timezone, dtStart2445));
+ values.put("dtend", get2445ToMillis(timezone, dtEnd2445));
+
+ int result = mDb.update("Events", values, "_id=" + eventId, null /* where args*/);
+ if (0 == result) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Could not update Events table with values " + values);
+ }
+ }
+ }
+
+ private void cleanInstancesTable() {
+ mDb.delete("Instances", null /* where clause */, null /* where args */);
+ }
+
+ private void updateTimezoneDatabaseVersion(String timeZoneDatabaseVersion) {
+ try {
+ mCalendarCache.writeTimezoneDatabaseVersion(timeZoneDatabaseVersion);
+ } catch (CalendarCache.CacheException e) {
+ Log.e(TAG, "Could not write timezone database version in the cache");
+ }
+ }
+
+ /**
+ * Check if we are in the same time zone
+ */
+ private boolean isSameTimezone() {
+ MetaData.Fields fields = mMetaData.getFields();
+ String localTimezone = TimeZone.getDefault().getID();
+ return TextUtils.equals(fields.timezone, localTimezone);
+ }
+
+ /**
+ * Check if the time zone database version is the same as the cached one
+ */
+ protected boolean isSameTimezoneDatabaseVersion() {
+ String timezoneDatabaseVersion = null;
+ try {
+ timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
+ } catch (CalendarCache.CacheException e) {
+ Log.e(TAG, "Could not read timezone database version from the cache");
+ return false;
+ }
+ return TextUtils.equals(timezoneDatabaseVersion, TimeUtils.getTimeZoneDatabaseVersion());
+ }
+
+ protected String getTimezoneDatabaseVersion() {
+ String timezoneDatabaseVersion = null;
+ try {
+ timezoneDatabaseVersion = mCalendarCache.readTimezoneDatabaseVersion();
+ } catch (CalendarCache.CacheException e) {
+ Log.e(TAG, "Could not read timezone database version from the cache");
+ return "";
+ }
+ Log.i(TAG, "timezoneDatabaseVersion = " + timezoneDatabaseVersion);
+ return timezoneDatabaseVersion;
+ }
+
+ private void regenerateInstancesTable() {
+ // The database timezone is different from the current timezone.
+ // Regenerate the Instances table for this month. Include events
+ // starting at the beginning of this month.
+ long now = System.currentTimeMillis();
+ Time time = new Time();
+ time.set(now);
+ time.monthDay = 1;
+ time.hour = 0;
+ time.minute = 0;
+ time.second = 0;
+ long begin = time.normalize(true);
+ long end = begin + MINIMUM_EXPANSION_SPAN;
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ handleInstanceQuery(qb, begin, end, new String[] { Instances._ID },
+ null /* selection */, null /* sort */, false /* searchByDayInsteadOfMillis */);
+
+ rescheduleMissedAlarms();
+ }
+
+ private void rescheduleMissedAlarms() {
+ AlarmManager manager = getAlarmManager();
+ if (manager != null) {
+ Context context = getContext();
+ ContentResolver cr = context.getContentResolver();
+ CalendarAlerts.rescheduleMissedAlarms(cr, context, manager);
+ }
+ }
+
+ /**
+ * Appends comma separated ids.
+ * @param ids Should not be empty
+ */
+ private void appendIds(StringBuilder sb, HashSet<Long> ids) {
+ for (long id : ids) {
+ sb.append(id).append(',');
+ }
+
+ sb.setLength(sb.length() - 1); // Yank the last comma
+ }
+
+ @Override
+ protected void notifyChange() {
+ // Note that semantics are changed: notification is for CONTENT_URI, not the specific
+ // Uri that was modified.
+ getContext().getContentResolver().notifyChange(Calendar.CONTENT_URI, null,
+ true /* syncToNetwork */);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "query: " + uri);
+ }
+
+ final SQLiteDatabase db = mDbHelper.getReadableDatabase();
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ String groupBy = null;
+ String limit = null; // Not currently implemented
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case SYNCSTATE:
+ return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs,
+ sortOrder);
+
+ case EVENTS:
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventsProjectionMap);
+ appendAccountFromParameter(qb, uri);
+ break;
+ case EVENTS_ID:
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventsProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
+ qb.appendWhere("_id=?");
+ break;
+
+ case EVENT_ENTITIES:
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventEntitiesProjectionMap);
+ appendAccountFromParameter(qb, uri);
+ break;
+ case EVENT_ENTITIES_ID:
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventEntitiesProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
+ qb.appendWhere("_id=?");
+ break;
+
+ case CALENDARS:
+ qb.setTables("Calendars");
+ appendAccountFromParameter(qb, uri);
+ break;
+ case CALENDARS_ID:
+ qb.setTables("Calendars");
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
+ qb.appendWhere("_id=?");
+ break;
+ case INSTANCES:
+ case INSTANCES_BY_DAY:
+ long begin;
+ long end;
+ try {
+ begin = Long.valueOf(uri.getPathSegments().get(2));
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Cannot parse begin "
+ + uri.getPathSegments().get(2));
+ }
+ try {
+ end = Long.valueOf(uri.getPathSegments().get(3));
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Cannot parse end "
+ + uri.getPathSegments().get(3));
+ }
+ return handleInstanceQuery(qb, begin, end, projection,
+ selection, sortOrder, match == INSTANCES_BY_DAY);
+ case EVENT_DAYS:
+ int startDay;
+ int endDay;
+ try {
+ startDay = Integer.valueOf(uri.getPathSegments().get(2));
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Cannot parse start day "
+ + uri.getPathSegments().get(2));
+ }
+ try {
+ endDay = Integer.valueOf(uri.getPathSegments().get(3));
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException("Cannot parse end day "
+ + uri.getPathSegments().get(3));
+ }
+ return handleEventDayQuery(qb, startDay, endDay, projection, selection);
+ case ATTENDEES:
+ qb.setTables("Attendees, Events");
+ qb.setProjectionMap(sAttendeesProjectionMap);
+ qb.appendWhere("Events._id=Attendees.event_id");
+ break;
+ case ATTENDEES_ID:
+ qb.setTables("Attendees, Events");
+ qb.setProjectionMap(sAttendeesProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
+ qb.appendWhere("Attendees._id=? AND Events._id=Attendees.event_id");
+ break;
+ case REMINDERS:
+ qb.setTables("Reminders");
+ break;
+ case REMINDERS_ID:
+ qb.setTables("Reminders, Events");
+ qb.setProjectionMap(sRemindersProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere("Reminders._id=? AND Events._id=Reminders.event_id");
+ break;
+ case CALENDAR_ALERTS:
+ qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sCalendarAlertsProjectionMap);
+ qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
+ "._id=CalendarAlerts.event_id");
+ break;
+ case CALENDAR_ALERTS_BY_INSTANCE:
+ qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sCalendarAlertsProjectionMap);
+ qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
+ "._id=CalendarAlerts.event_id");
+ groupBy = CalendarAlerts.EVENT_ID + "," + CalendarAlerts.BEGIN;
+ break;
+ case CALENDAR_ALERTS_ID:
+ qb.setTables("CalendarAlerts, " + CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sCalendarAlertsProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(CalendarDatabaseHelper.Views.EVENTS +
+ "._id=CalendarAlerts.event_id AND CalendarAlerts._id=?");
+ break;
+ case EXTENDED_PROPERTIES:
+ qb.setTables("ExtendedProperties");
+ break;
+ case EXTENDED_PROPERTIES_ID:
+ qb.setTables("ExtendedProperties");
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getPathSegments().get(1));
+ qb.appendWhere("ExtendedProperties._id=?");
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+
+ // run the query
+ return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
+ }
+
+ private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder, String groupBy,
+ String limit) {
+ final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
+ sortOrder, limit);
+ if (c != null) {
+ // TODO: is this the right notification Uri?
+ c.setNotificationUri(getContext().getContentResolver(), Calendar.Events.CONTENT_URI);
+ }
+ return c;
+ }
+
+ /*
+ * Fills the Instances table, if necessary, for the given range and then
+ * queries the Instances table.
+ *
+ * @param qb The query
+ * @param rangeBegin start of range (Julian days or ms)
+ * @param rangeEnd end of range (Julian days or ms)
+ * @param projection The projection
+ * @param selection The selection
+ * @param sort How to sort
+ * @param searchByDay if true, range is in Julian days, if false, range is in ms
+ * @return
+ */
+ private Cursor handleInstanceQuery(SQLiteQueryBuilder qb, long rangeBegin,
+ long rangeEnd, String[] projection,
+ String selection, String sort, boolean searchByDay) {
+
+ qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
+ "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
+ qb.setProjectionMap(sInstancesProjectionMap);
+ if (searchByDay) {
+ // Convert the first and last Julian day range to a range that uses
+ // UTC milliseconds.
+ Time time = new Time();
+ long beginMs = time.setJulianDay((int) rangeBegin);
+ // We add one to lastDay because the time is set to 12am on the given
+ // Julian day and we want to include all the events on the last day.
+ long endMs = time.setJulianDay((int) rangeEnd + 1);
+ // will lock the database.
+ acquireInstanceRange(beginMs, endMs, true /* use minimum expansion window */);
+ qb.appendWhere("startDay<=? AND endDay>=?");
+ } else {
+ // will lock the database.
+ acquireInstanceRange(rangeBegin, rangeEnd, true /* use minimum expansion window */);
+ qb.appendWhere("begin<=? AND end>=?");
+ }
+ String selectionArgs[] = new String[] {String.valueOf(rangeEnd),
+ String.valueOf(rangeBegin)};
+ return qb.query(mDb, projection, selection, selectionArgs, null /* groupBy */,
+ null /* having */, sort);
+ }
+
+ private Cursor handleEventDayQuery(SQLiteQueryBuilder qb, int begin, int end,
+ String[] projection, String selection) {
+ qb.setTables("Instances INNER JOIN Events ON (Instances.event_id=Events._id) " +
+ "INNER JOIN Calendars ON (Events.calendar_id = Calendars._id)");
+ qb.setProjectionMap(sInstancesProjectionMap);
+ // Convert the first and last Julian day range to a range that uses
+ // UTC milliseconds.
+ Time time = new Time();
+ long beginMs = time.setJulianDay((int) begin);
+ // We add one to lastDay because the time is set to 12am on the given
+ // Julian day and we want to include all the events on the last day.
+ long endMs = time.setJulianDay((int) end + 1);
+
+ acquireInstanceRange(beginMs, endMs, true);
+ qb.appendWhere("startDay<=? AND endDay>=?");
+ String selectionArgs[] = new String[] {String.valueOf(end), String.valueOf(begin)};
+
+ return qb.query(mDb, projection, selection, selectionArgs,
+ Instances.START_DAY /* groupBy */, null /* having */, null);
+ }
+
+ /**
+ * Ensure that the date range given has all elements in the instance
+ * table. Acquires the database lock and calls {@link #acquireInstanceRangeLocked}.
+ *
+ * @param begin start of range (ms)
+ * @param end end of range (ms)
+ * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
+ */
+ private void acquireInstanceRange(final long begin,
+ final long end,
+ final boolean useMinimumExpansionWindow) {
+ mDb.beginTransaction();
+ try {
+ acquireInstanceRangeLocked(begin, end, useMinimumExpansionWindow);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+ }
+
+ /**
+ * Ensure that the date range given has all elements in the instance
+ * table. The database lock must be held when calling this method.
+ *
+ * @param begin start of range (ms)
+ * @param end end of range (ms)
+ * @param useMinimumExpansionWindow expand by at least MINIMUM_EXPANSION_SPAN
+ */
+ private void acquireInstanceRangeLocked(long begin, long end,
+ boolean useMinimumExpansionWindow) {
+ long expandBegin = begin;
+ long expandEnd = end;
+
+ if (useMinimumExpansionWindow) {
+ // if we end up having to expand events into the instances table, expand
+ // events for a minimal amount of time, so we do not have to perform
+ // expansions frequently.
+ long span = end - begin;
+ if (span < MINIMUM_EXPANSION_SPAN) {
+ long additionalRange = (MINIMUM_EXPANSION_SPAN - span) / 2;
+ expandBegin -= additionalRange;
+ expandEnd += additionalRange;
+ }
+ }
+
+ // Check if the timezone has changed.
+ // We do this check here because the database is locked and we can
+ // safely delete all the entries in the Instances table.
+ MetaData.Fields fields = mMetaData.getFieldsLocked();
+ String dbTimezone = fields.timezone;
+ long maxInstance = fields.maxInstance;
+ long minInstance = fields.minInstance;
+ String localTimezone = TimeZone.getDefault().getID();
+ boolean timezoneChanged = (dbTimezone == null) || !dbTimezone.equals(localTimezone);
+
+ if (maxInstance == 0 || timezoneChanged) {
+ // Empty the Instances table and expand from scratch.
+ mDb.execSQL("DELETE FROM Instances;");
+ if (Config.LOGV) {
+ Log.v(TAG, "acquireInstanceRangeLocked() deleted Instances,"
+ + " timezone changed: " + timezoneChanged);
+ }
+ expandInstanceRangeLocked(expandBegin, expandEnd, localTimezone);
+
+ mMetaData.writeLocked(localTimezone, expandBegin, expandEnd);
+ return;
+ }
+
+ // If the desired range [begin, end] has already been
+ // expanded, then simply return. The range is inclusive, that is,
+ // events that touch either endpoint are included in the expansion.
+ // This means that a zero-duration event that starts and ends at
+ // the endpoint will be included.
+ // We use [begin, end] here and not [expandBegin, expandEnd] for
+ // checking the range because a common case is for the client to
+ // request successive days or weeks, for example. If we checked
+ // that the expanded range [expandBegin, expandEnd] then we would
+ // always be expanding because there would always be one more day
+ // or week that hasn't been expanded.
+ if ((begin >= minInstance) && (end <= maxInstance)) {
+ if (Config.LOGV) {
+ Log.v(TAG, "Canceled instance query (" + expandBegin + ", " + expandEnd
+ + ") falls within previously expanded range.");
+ }
+ return;
+ }
+
+ // If the requested begin point has not been expanded, then include
+ // more events than requested in the expansion (use "expandBegin").
+ if (begin < minInstance) {
+ expandInstanceRangeLocked(expandBegin, minInstance, localTimezone);
+ minInstance = expandBegin;
+ }
+
+ // If the requested end point has not been expanded, then include
+ // more events than requested in the expansion (use "expandEnd").
+ if (end > maxInstance) {
+ expandInstanceRangeLocked(maxInstance, expandEnd, localTimezone);
+ maxInstance = expandEnd;
+ }
+
+ // Update the bounds on the Instances table.
+ mMetaData.writeLocked(localTimezone, minInstance, maxInstance);
+ }
+
+ private static final String[] EXPAND_COLUMNS = new String[] {
+ Events._ID,
+ Events._SYNC_ID,
+ Events.STATUS,
+ Events.DTSTART,
+ Events.DTEND,
+ Events.EVENT_TIMEZONE,
+ Events.RRULE,
+ Events.RDATE,
+ Events.EXRULE,
+ Events.EXDATE,
+ Events.DURATION,
+ Events.ALL_DAY,
+ Events.ORIGINAL_EVENT,
+ Events.ORIGINAL_INSTANCE_TIME
+ };
+
+ /**
+ * Make instances for the given range.
+ */
+ private void expandInstanceRangeLocked(long begin, long end, String localTimezone) {
+
+ if (PROFILE) {
+ Debug.startMethodTracing("expandInstanceRangeLocked");
+ }
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Expanding events between " + begin + " and " + end);
+ }
+
+ Cursor entries = getEntries(begin, end);
+ try {
+ performInstanceExpansion(begin, end, localTimezone, entries);
+ } finally {
+ if (entries != null) {
+ entries.close();
+ }
+ }
+ if (PROFILE) {
+ Debug.stopMethodTracing();
+ }
+ }
+
+ /**
+ * Get all entries affecting the given window.
+ * @param begin Window start (ms).
+ * @param end Window end (ms).
+ * @return Cursor for the entries; caller must close it.
+ */
+ private Cursor getEntries(long begin, long end) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventsProjectionMap);
+
+ String beginString = String.valueOf(begin);
+ String endString = String.valueOf(end);
+
+ // grab recurrence exceptions that fall outside our expansion window but modify
+ // recurrences that do fall within our window. we won't insert these into the output
+ // set of instances, but instead will just add them to our cancellations list, so we
+ // can cancel the correct recurrence expansion instances.
+ // we don't have originalInstanceDuration or end time. for now, assume the original
+ // instance lasts no longer than 1 week.
+ // TODO: compute the originalInstanceEndTime or get this from the server.
+ qb.appendWhere("(dtstart <= ? AND (lastDate IS NULL OR lastDate >= ?)) OR " +
+ "(originalInstanceTime IS NOT NULL AND originalInstanceTime <= ? AND " +
+ "originalInstanceTime >= ?)");
+ String selectionArgs[] = new String[] {endString, beginString, endString,
+ String.valueOf(begin - MAX_ASSUMED_DURATION)};
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Retrieving events to expand: " + qb.toString());
+ }
+
+ return qb.query(mDb, EXPAND_COLUMNS, null /* selection */,
+ selectionArgs, null /* groupBy */,
+ null /* having */, null /* sortOrder */);
+ }
+
+ /**
+ * Perform instance expansion on the given entries.
+ * @param begin Window start (ms).
+ * @param end Window end (ms).
+ * @param localTimezone
+ * @param entries The entries to process.
+ */
+ private void performInstanceExpansion(long begin, long end, String localTimezone,
+ Cursor entries) {
+ RecurrenceProcessor rp = new RecurrenceProcessor();
+
+ int statusColumn = entries.getColumnIndex(Events.STATUS);
+ int dtstartColumn = entries.getColumnIndex(Events.DTSTART);
+ int dtendColumn = entries.getColumnIndex(Events.DTEND);
+ int eventTimezoneColumn = entries.getColumnIndex(Events.EVENT_TIMEZONE);
+ int durationColumn = entries.getColumnIndex(Events.DURATION);
+ int rruleColumn = entries.getColumnIndex(Events.RRULE);
+ int rdateColumn = entries.getColumnIndex(Events.RDATE);
+ int exruleColumn = entries.getColumnIndex(Events.EXRULE);
+ int exdateColumn = entries.getColumnIndex(Events.EXDATE);
+ int allDayColumn = entries.getColumnIndex(Events.ALL_DAY);
+ int idColumn = entries.getColumnIndex(Events._ID);
+ int syncIdColumn = entries.getColumnIndex(Events._SYNC_ID);
+ int originalEventColumn = entries.getColumnIndex(Events.ORIGINAL_EVENT);
+ int originalInstanceTimeColumn = entries.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
+
+ ContentValues initialValues;
+ EventInstancesMap instancesMap = new EventInstancesMap();
+
+ Duration duration = new Duration();
+ Time eventTime = new Time();
+
+ // Invariant: entries contains all events that affect the current
+ // window. It consists of:
+ // a) Individual events that fall in the window. These will be
+ // displayed.
+ // b) Recurrences that included the window. These will be displayed
+ // if not canceled.
+ // c) Recurrence exceptions that fall in the window. These will be
+ // displayed if not cancellations.
+ // d) Recurrence exceptions that modify an instance inside the
+ // window (subject to 1 week assumption above), but are outside
+ // the window. These will not be displayed. Cases c and d are
+ // distingushed by the start / end time.
+
+ while (entries.moveToNext()) {
+ try {
+ initialValues = null;
+
+ boolean allDay = entries.getInt(allDayColumn) != 0;
+
+ String eventTimezone = entries.getString(eventTimezoneColumn);
+ if (allDay || TextUtils.isEmpty(eventTimezone)) {
+ // in the events table, allDay events start at midnight.
+ // this forces them to stay at midnight for all day events
+ // TODO: check that this actually does the right thing.
+ eventTimezone = Time.TIMEZONE_UTC;
+ }
+
+ long dtstartMillis = entries.getLong(dtstartColumn);
+ Long eventId = Long.valueOf(entries.getLong(idColumn));
+
+ String durationStr = entries.getString(durationColumn);
+ if (durationStr != null) {
+ try {
+ duration.parse(durationStr);
+ }
+ catch (DateException e) {
+ Log.w(TAG, "error parsing duration for event "
+ + eventId + "'" + durationStr + "'", e);
+ duration.sign = 1;
+ duration.weeks = 0;
+ duration.days = 0;
+ duration.hours = 0;
+ duration.minutes = 0;
+ duration.seconds = 0;
+ durationStr = "+P0S";
+ }
+ }
+
+ String syncId = entries.getString(syncIdColumn);
+ String originalEvent = entries.getString(originalEventColumn);
+
+ long originalInstanceTimeMillis = -1;
+ if (!entries.isNull(originalInstanceTimeColumn)) {
+ originalInstanceTimeMillis= entries.getLong(originalInstanceTimeColumn);
+ }
+ int status = entries.getInt(statusColumn);
+
+ String rruleStr = entries.getString(rruleColumn);
+ String rdateStr = entries.getString(rdateColumn);
+ String exruleStr = entries.getString(exruleColumn);
+ String exdateStr = entries.getString(exdateColumn);
+
+ RecurrenceSet recur = new RecurrenceSet(rruleStr, rdateStr, exruleStr, exdateStr);
+
+ if (recur.hasRecurrence()) {
+ // the event is repeating
+
+ if (status == Events.STATUS_CANCELED) {
+ // should not happen!
+ Log.e(TAG, "Found canceled recurring event in "
+ + "Events table. Ignoring.");
+ continue;
+ }
+
+ // need to parse the event into a local calendar.
+ eventTime.timezone = eventTimezone;
+ eventTime.set(dtstartMillis);
+ eventTime.allDay = allDay;
+
+ if (durationStr == null) {
+ // should not happen.
+ Log.e(TAG, "Repeating event has no duration -- "
+ + "should not happen.");
+ if (allDay) {
+ // set to one day.
+ duration.sign = 1;
+ duration.weeks = 0;
+ duration.days = 1;
+ duration.hours = 0;
+ duration.minutes = 0;
+ duration.seconds = 0;
+ durationStr = "+P1D";
+ } else {
+ // compute the duration from dtend, if we can.
+ // otherwise, use 0s.
+ duration.sign = 1;
+ duration.weeks = 0;
+ duration.days = 0;
+ duration.hours = 0;
+ duration.minutes = 0;
+ if (!entries.isNull(dtendColumn)) {
+ long dtendMillis = entries.getLong(dtendColumn);
+ duration.seconds = (int) ((dtendMillis - dtstartMillis) / 1000);
+ durationStr = "+P" + duration.seconds + "S";
+ } else {
+ duration.seconds = 0;
+ durationStr = "+P0S";
+ }
+ }
+ }
+
+ long[] dates;
+ dates = rp.expand(eventTime, recur, begin, end);
+
+ // Initialize the "eventTime" timezone outside the loop.
+ // This is used in computeTimezoneDependentFields().
+ if (allDay) {
+ eventTime.timezone = Time.TIMEZONE_UTC;
+ } else {
+ eventTime.timezone = localTimezone;
+ }
+
+ long durationMillis = duration.getMillis();
+ for (long date : dates) {
+ initialValues = new ContentValues();
+ initialValues.put(Instances.EVENT_ID, eventId);
+
+ initialValues.put(Instances.BEGIN, date);
+ long dtendMillis = date + durationMillis;
+ initialValues.put(Instances.END, dtendMillis);
+
+ computeTimezoneDependentFields(date, dtendMillis,
+ eventTime, initialValues);
+ instancesMap.add(syncId, initialValues);
+ }
+ } else {
+ // the event is not repeating
+ initialValues = new ContentValues();
+
+ // if this event has an "original" field, then record
+ // that we need to cancel the original event (we can't
+ // do that here because the order of this loop isn't
+ // defined)
+ if (originalEvent != null && originalInstanceTimeMillis != -1) {
+ initialValues.put(Events.ORIGINAL_EVENT, originalEvent);
+ initialValues.put(Events.ORIGINAL_INSTANCE_TIME,
+ originalInstanceTimeMillis);
+ initialValues.put(Events.STATUS, status);
+ }
+
+ long dtendMillis = dtstartMillis;
+ if (durationStr == null) {
+ if (!entries.isNull(dtendColumn)) {
+ dtendMillis = entries.getLong(dtendColumn);
+ }
+ } else {
+ dtendMillis = duration.addTo(dtstartMillis);
+ }
+
+ // this non-recurring event might be a recurrence exception that doesn't
+ // actually fall within our expansion window, but instead was selected
+ // so we can correctly cancel expanded recurrence instances below. do not
+ // add events to the instances map if they don't actually fall within our
+ // expansion window.
+ if ((dtendMillis < begin) || (dtstartMillis > end)) {
+ if (originalEvent != null && originalInstanceTimeMillis != -1) {
+ initialValues.put(Events.STATUS, Events.STATUS_CANCELED);
+ } else {
+ Log.w(TAG, "Unexpected event outside window: " + syncId);
+ continue;
+ }
+ }
+
+ initialValues.put(Instances.EVENT_ID, eventId);
+ initialValues.put(Instances.BEGIN, dtstartMillis);
+
+ initialValues.put(Instances.END, dtendMillis);
+
+ if (allDay) {
+ eventTime.timezone = Time.TIMEZONE_UTC;
+ } else {
+ eventTime.timezone = localTimezone;
+ }
+ computeTimezoneDependentFields(dtstartMillis, dtendMillis,
+ eventTime, initialValues);
+
+ instancesMap.add(syncId, initialValues);
+ }
+ } catch (DateException e) {
+ Log.w(TAG, "RecurrenceProcessor error ", e);
+ } catch (TimeFormatException e) {
+ Log.w(TAG, "RecurrenceProcessor error ", e);
+ }
+ }
+
+ // Invariant: instancesMap contains all instances that affect the
+ // window, indexed by original sync id. It consists of:
+ // a) Individual events that fall in the window. They have:
+ // EVENT_ID, BEGIN, END
+ // b) Instances of recurrences that fall in the window. They may
+ // be subject to exceptions. They have:
+ // EVENT_ID, BEGIN, END
+ // c) Exceptions that fall in the window. They have:
+ // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS (since they can
+ // be a modification or cancellation), EVENT_ID, BEGIN, END
+ // d) Recurrence exceptions that modify an instance inside the
+ // window but fall outside the window. They have:
+ // ORIGINAL_EVENT, ORIGINAL_INSTANCE_TIME, STATUS =
+ // STATUS_CANCELED, EVENT_ID, BEGIN, END
+
+ // First, delete the original instances corresponding to recurrence
+ // exceptions. We do this by iterating over the list and for each
+ // recurrence exception, we search the list for an instance with a
+ // matching "original instance time". If we find such an instance,
+ // we remove it from the list. If we don't find such an instance
+ // then we cancel the recurrence exception.
+ Set<String> keys = instancesMap.keySet();
+ for (String syncId : keys) {
+ InstancesList list = instancesMap.get(syncId);
+ for (ContentValues values : list) {
+
+ // If this instance is not a recurrence exception, then
+ // skip it.
+ if (!values.containsKey(Events.ORIGINAL_EVENT)) {
+ continue;
+ }
+
+ String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
+ long originalTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ InstancesList originalList = instancesMap.get(originalEvent);
+ if (originalList == null) {
+ // The original recurrence is not present, so don't try canceling it.
+ continue;
+ }
+
+ // Search the original event for a matching original
+ // instance time. If there is a matching one, then remove
+ // the original one. We do this both for exceptions that
+ // change the original instance as well as for exceptions
+ // that delete the original instance.
+ for (int num = originalList.size() - 1; num >= 0; num--) {
+ ContentValues originalValues = originalList.get(num);
+ long beginTime = originalValues.getAsLong(Instances.BEGIN);
+ if (beginTime == originalTime) {
+ // We found the original instance, so remove it.
+ originalList.remove(num);
+ }
+ }
+ }
+ }
+
+ // Invariant: instancesMap contains filtered instances.
+ // It consists of:
+ // a) Individual events that fall in the window.
+ // b) Instances of recurrences that fall in the window and have not
+ // been subject to exceptions.
+ // c) Exceptions that fall in the window. They will have
+ // STATUS_CANCELED if they are cancellations.
+ // d) Recurrence exceptions that modify an instance inside the
+ // window but fall outside the window. These are STATUS_CANCELED.
+
+ // Now do the inserts. Since the db lock is held when this method is executed,
+ // this will be done in a transaction.
+ // NOTE: if there is lock contention (e.g., a sync is trying to merge into the db
+ // while the calendar app is trying to query the db (expanding instances)), we will
+ // not be "polite" and yield the lock until we're done. This will favor local query
+ // operations over sync/write operations.
+ for (String syncId : keys) {
+ InstancesList list = instancesMap.get(syncId);
+ for (ContentValues values : list) {
+
+ // If this instance was cancelled then don't create a new
+ // instance.
+ Integer status = values.getAsInteger(Events.STATUS);
+ if (status != null && status == Events.STATUS_CANCELED) {
+ continue;
+ }
+
+ // Remove these fields before inserting a new instance
+ values.remove(Events.ORIGINAL_EVENT);
+ values.remove(Events.ORIGINAL_INSTANCE_TIME);
+ values.remove(Events.STATUS);
+
+ mDbHelper.instancesInsert(values);
+ }
+ }
+ }
+
+ /**
+ * Computes the timezone-dependent fields of an instance of an event and
+ * updates the "values" map to contain those fields.
+ *
+ * @param begin the start time of the instance (in UTC milliseconds)
+ * @param end the end time of the instance (in UTC milliseconds)
+ * @param local a Time object with the timezone set to the local timezone
+ * @param values a map that will contain the timezone-dependent fields
+ */
+ private void computeTimezoneDependentFields(long begin, long end,
+ Time local, ContentValues values) {
+ local.set(begin);
+ int startDay = Time.getJulianDay(begin, local.gmtoff);
+ int startMinute = local.hour * 60 + local.minute;
+
+ local.set(end);
+ int endDay = Time.getJulianDay(end, local.gmtoff);
+ int endMinute = local.hour * 60 + local.minute;
+
+ // Special case for midnight, which has endMinute == 0. Change
+ // that to +24 hours on the previous day to make everything simpler.
+ // Exception: if start and end minute are both 0 on the same day,
+ // then leave endMinute alone.
+ if (endMinute == 0 && endDay > startDay) {
+ endMinute = 24 * 60;
+ endDay -= 1;
+ }
+
+ values.put(Instances.START_DAY, startDay);
+ values.put(Instances.END_DAY, endDay);
+ values.put(Instances.START_MINUTE, startMinute);
+ values.put(Instances.END_MINUTE, endMinute);
+ }
+
+ @Override
+ public String getType(Uri url) {
+ int match = sUriMatcher.match(url);
+ switch (match) {
+ case EVENTS:
+ return "vnd.android.cursor.dir/event";
+ case EVENTS_ID:
+ return "vnd.android.cursor.item/event";
+ case REMINDERS:
+ return "vnd.android.cursor.dir/reminder";
+ case REMINDERS_ID:
+ return "vnd.android.cursor.item/reminder";
+ case CALENDAR_ALERTS:
+ return "vnd.android.cursor.dir/calendar-alert";
+ case CALENDAR_ALERTS_BY_INSTANCE:
+ return "vnd.android.cursor.dir/calendar-alert-by-instance";
+ case CALENDAR_ALERTS_ID:
+ return "vnd.android.cursor.item/calendar-alert";
+ case INSTANCES:
+ case INSTANCES_BY_DAY:
+ case EVENT_DAYS:
+ return "vnd.android.cursor.dir/event-instance";
+ default:
+ throw new IllegalArgumentException("Unknown URL " + url);
+ }
+ }
+
+ public static boolean isRecurrenceEvent(ContentValues values) {
+ return (!TextUtils.isEmpty(values.getAsString(Events.RRULE))||
+ !TextUtils.isEmpty(values.getAsString(Events.RDATE))||
+ !TextUtils.isEmpty(values.getAsString(Events.ORIGINAL_EVENT)));
+ }
+
+ @Override
+ protected Uri insertInTransaction(Uri uri, ContentValues values) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "insertInTransaction: " + uri);
+ }
+
+ final boolean callerIsSyncAdapter =
+ readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
+
+ final int match = sUriMatcher.match(uri);
+ long id = 0;
+
+ switch (match) {
+ case SYNCSTATE:
+ id = mDbHelper.getSyncState().insert(mDb, values);
+ break;
+ case EVENTS:
+ if (!callerIsSyncAdapter) {
+ values.put(Events._SYNC_DIRTY, 1);
+ }
+ if (!values.containsKey(Events.DTSTART)) {
+ throw new RuntimeException("DTSTART field missing from event");
+ }
+ // TODO: avoid the call to updateBundleFromEvent if this is just finding local
+ // changes.
+ // TODO: do we really need to make a copy?
+ ContentValues updatedValues = updateContentValuesFromEvent(values);
+ if (updatedValues == null) {
+ throw new RuntimeException("Could not insert event.");
+ // return null;
+ }
+ String owner = null;
+ if (updatedValues.containsKey(Events.CALENDAR_ID) &&
+ !updatedValues.containsKey(Events.ORGANIZER)) {
+ owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
+ // TODO: This isn't entirely correct. If a guest is adding a recurrence
+ // exception to an event, the organizer should stay the original organizer.
+ // This value doesn't go to the server and it will get fixed on sync,
+ // so it shouldn't really matter.
+ if (owner != null) {
+ updatedValues.put(Events.ORGANIZER, owner);
+ }
+ }
+
+ id = mDbHelper.eventsInsert(updatedValues);
+ if (id != -1) {
+ updateEventRawTimesLocked(id, updatedValues);
+ updateInstancesLocked(updatedValues, id, true /* new event */, mDb);
+
+ // If we inserted a new event that specified the self-attendee
+ // status, then we need to add an entry to the attendees table.
+ if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
+ int status = values.getAsInteger(Events.SELF_ATTENDEE_STATUS);
+ if (owner == null) {
+ owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
+ }
+ createAttendeeEntry(id, status, owner);
+ }
+ triggerAppWidgetUpdate(id);
+ }
+ break;
+ case CALENDARS:
+ Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
+ if (syncEvents != null && syncEvents == 1) {
+ String accountName = values.getAsString(Calendars._SYNC_ACCOUNT);
+ String accountType = values.getAsString(
+ Calendars._SYNC_ACCOUNT_TYPE);
+ final Account account = new Account(accountName, accountType);
+ String calendarUrl = values.getAsString(Calendars.URL);
+ mDbHelper.scheduleSync(account, false /* two-way sync */, calendarUrl);
+ }
+ id = mDbHelper.calendarsInsert(values);
+ break;
+ case ATTENDEES:
+ if (!values.containsKey(Attendees.EVENT_ID)) {
+ throw new IllegalArgumentException("Attendees values must "
+ + "contain an event_id");
+ }
+ id = mDbHelper.attendeesInsert(values);
+ if (!callerIsSyncAdapter) {
+ setEventDirty(values.getAsInteger(Attendees.EVENT_ID));
+ }
+
+ // Copy the attendee status value to the Events table.
+ updateEventAttendeeStatus(mDb, values);
+ break;
+ case REMINDERS:
+ if (!values.containsKey(Reminders.EVENT_ID)) {
+ throw new IllegalArgumentException("Reminders values must "
+ + "contain an event_id");
+ }
+ id = mDbHelper.remindersInsert(values);
+ if (!callerIsSyncAdapter) {
+ setEventDirty(values.getAsInteger(Reminders.EVENT_ID));
+ }
+
+ // Schedule another event alarm, if necessary
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "insertInternal() changing reminder");
+ }
+ scheduleNextAlarm(false /* do not remove alarms */);
+ break;
+ case CALENDAR_ALERTS:
+ if (!values.containsKey(CalendarAlerts.EVENT_ID)) {
+ throw new IllegalArgumentException("CalendarAlerts values must "
+ + "contain an event_id");
+ }
+ id = mDbHelper.calendarAlertsInsert(values);
+ // Note: dirty bit is not set for Alerts because it is not synced.
+ // It is generated from Reminders, which is synced.
+ break;
+ case EXTENDED_PROPERTIES:
+ if (!values.containsKey(Calendar.ExtendedProperties.EVENT_ID)) {
+ throw new IllegalArgumentException("ExtendedProperties values must "
+ + "contain an event_id");
+ }
+ id = mDbHelper.extendedPropertiesInsert(values);
+ if (!callerIsSyncAdapter) {
+ setEventDirty(values.getAsInteger(Calendar.ExtendedProperties.EVENT_ID));
+ }
+ break;
+ case DELETED_EVENTS:
+ case EVENTS_ID:
+ case REMINDERS_ID:
+ case CALENDAR_ALERTS_ID:
+ case EXTENDED_PROPERTIES_ID:
+ case INSTANCES:
+ case INSTANCES_BY_DAY:
+ case EVENT_DAYS:
+ throw new UnsupportedOperationException("Cannot insert into that URL: " + uri);
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+
+ if (id < 0) {
+ return null;
+ }
+
+ return ContentUris.withAppendedId(uri, id);
+ }
+
+ private void setEventDirty(int eventId) {
+ mDb.execSQL("UPDATE Events SET _sync_dirty=1 where _id=?", new Integer[] {eventId});
+ }
+
+ /**
+ * Gets the calendar's owner for an event.
+ * @param calId
+ * @return email of owner or null
+ */
+ private String getOwner(long calId) {
+ // Get the email address of this user from this Calendar
+ String emailAddress = null;
+ Cursor cursor = null;
+ try {
+ cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
+ new String[] { Calendars.OWNER_ACCOUNT },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if (cursor == null || !cursor.moveToFirst()) {
+ Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
+ return null;
+ }
+ emailAddress = cursor.getString(0);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return emailAddress;
+ }
+
+ /**
+ * Creates an entry in the Attendees table that refers to the given event
+ * and that has the given response status.
+ *
+ * @param eventId the event id that the new entry in the Attendees table
+ * should refer to
+ * @param status the response status
+ * @param emailAddress the email of the attendee
+ */
+ private void createAttendeeEntry(long eventId, int status, String emailAddress) {
+ ContentValues values = new ContentValues();
+ values.put(Attendees.EVENT_ID, eventId);
+ values.put(Attendees.ATTENDEE_STATUS, status);
+ values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_NONE);
+ // TODO: The relationship could actually be ORGANIZER, but it will get straightened out
+ // on sync.
+ values.put(Attendees.ATTENDEE_RELATIONSHIP,
+ Attendees.RELATIONSHIP_ATTENDEE);
+ values.put(Attendees.ATTENDEE_EMAIL, emailAddress);
+
+ // We don't know the ATTENDEE_NAME but that will be filled in by the
+ // server and sent back to us.
+ mDbHelper.attendeesInsert(values);
+ }
+
+ /**
+ * Updates the attendee status in the Events table to be consistent with
+ * the value in the Attendees table.
+ *
+ * @param db the database
+ * @param attendeeValues the column values for one row in the Attendees
+ * table.
+ */
+ private void updateEventAttendeeStatus(SQLiteDatabase db, ContentValues attendeeValues) {
+ // Get the event id for this attendee
+ long eventId = attendeeValues.getAsLong(Attendees.EVENT_ID);
+
+ if (MULTIPLE_ATTENDEES_PER_EVENT) {
+ // Get the calendar id for this event
+ Cursor cursor = null;
+ long calId;
+ try {
+ cursor = query(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ new String[] { Events.CALENDAR_ID },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if (cursor == null || !cursor.moveToFirst()) {
+ Log.d(TAG, "Couldn't find " + eventId + " in Events table");
+ return;
+ }
+ calId = cursor.getLong(0);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // Get the owner email for this Calendar
+ String calendarEmail = null;
+ cursor = null;
+ try {
+ cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
+ new String[] { Calendars.OWNER_ACCOUNT },
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+ if (cursor == null || !cursor.moveToFirst()) {
+ Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
+ return;
+ }
+ calendarEmail = cursor.getString(0);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ if (calendarEmail == null) {
+ return;
+ }
+
+ // Get the email address for this attendee
+ String attendeeEmail = null;
+ if (attendeeValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+ attendeeEmail = attendeeValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ }
+
+ // If the attendee email does not match the calendar email, then this
+ // attendee is not the owner of this calendar so we don't update the
+ // selfAttendeeStatus in the event.
+ if (!calendarEmail.equals(attendeeEmail)) {
+ return;
+ }
+ }
+
+ int status = Attendees.ATTENDEE_STATUS_NONE;
+ if (attendeeValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
+ int rel = attendeeValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+ if (rel == Attendees.RELATIONSHIP_ORGANIZER) {
+ status = Attendees.ATTENDEE_STATUS_ACCEPTED;
+ }
+ }
+
+ if (attendeeValues.containsKey(Attendees.ATTENDEE_STATUS)) {
+ status = attendeeValues.getAsInteger(Attendees.ATTENDEE_STATUS);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Events.SELF_ATTENDEE_STATUS, status);
+ db.update("Events", values, "_id=?", new String[] {String.valueOf(eventId)});
+ }
+
+ /**
+ * Updates the instances table when an event is added or updated.
+ * @param values The new values of the event.
+ * @param rowId The database row id of the event.
+ * @param newEvent true if the event is new.
+ * @param db The database
+ */
+ private void updateInstancesLocked(ContentValues values,
+ long rowId,
+ boolean newEvent,
+ SQLiteDatabase db) {
+
+ // If there are no expanded Instances, then return.
+ MetaData.Fields fields = mMetaData.getFieldsLocked();
+ if (fields.maxInstance == 0) {
+ return;
+ }
+
+ Long dtstartMillis = values.getAsLong(Events.DTSTART);
+ if (dtstartMillis == null) {
+ if (newEvent) {
+ // must be present for a new event.
+ throw new RuntimeException("DTSTART missing.");
+ }
+ if (Config.LOGV) Log.v(TAG, "Missing DTSTART. "
+ + "No need to update instance.");
+ return;
+ }
+
+ Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
+ Long originalInstanceTime = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+
+ if (!newEvent) {
+ // Want to do this for regular event, recurrence, or exception.
+ // For recurrence or exception, more deletion may happen below if we
+ // do an instance expansion. This deletion will suffice if the exception
+ // is moved outside the window, for instance.
+ db.delete("Instances", "event_id=?", new String[] {String.valueOf(rowId)});
+ }
+
+ if (isRecurrenceEvent(values)) {
+ // The recurrence or exception needs to be (re-)expanded if:
+ // a) Exception or recurrence that falls inside window
+ boolean insideWindow = dtstartMillis <= fields.maxInstance &&
+ (lastDateMillis == null || lastDateMillis >= fields.minInstance);
+ // b) Exception that affects instance inside window
+ // These conditions match the query in getEntries
+ // See getEntries comment for explanation of subtracting 1 week.
+ boolean affectsWindow = originalInstanceTime != null &&
+ originalInstanceTime <= fields.maxInstance &&
+ originalInstanceTime >= fields.minInstance - MAX_ASSUMED_DURATION;
+ if (insideWindow || affectsWindow) {
+ updateRecurrenceInstancesLocked(values, rowId, db);
+ }
+ // TODO: an exception creation or update could be optimized by
+ // updating just the affected instances, instead of regenerating
+ // the recurrence.
+ return;
+ }
+
+ Long dtendMillis = values.getAsLong(Events.DTEND);
+ if (dtendMillis == null) {
+ dtendMillis = dtstartMillis;
+ }
+
+ // if the event is in the expanded range, insert
+ // into the instances table.
+ // TODO: deal with durations. currently, durations are only used in
+ // recurrences.
+
+ if (dtstartMillis <= fields.maxInstance && dtendMillis >= fields.minInstance) {
+ ContentValues instanceValues = new ContentValues();
+ instanceValues.put(Instances.EVENT_ID, rowId);
+ instanceValues.put(Instances.BEGIN, dtstartMillis);
+ instanceValues.put(Instances.END, dtendMillis);
+
+ boolean allDay = false;
+ Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
+ if (allDayInteger != null) {
+ allDay = allDayInteger != 0;
+ }
+
+ // Update the timezone-dependent fields.
+ Time local = new Time();
+ if (allDay) {
+ local.timezone = Time.TIMEZONE_UTC;
+ } else {
+ local.timezone = fields.timezone;
+ }
+
+ computeTimezoneDependentFields(dtstartMillis, dtendMillis, local, instanceValues);
+ mDbHelper.instancesInsert(instanceValues);
+ }
+ }
+
+ /**
+ * Determines the recurrence entries associated with a particular recurrence.
+ * This set is the base recurrence and any exception.
+ *
+ * Normally the entries are indicated by the sync id of the base recurrence
+ * (which is the originalEvent in the exceptions).
+ * However, a complication is that a recurrence may not yet have a sync id.
+ * In that case, the recurrence is specified by the rowId.
+ *
+ * @param recurrenceSyncId The sync id of the base recurrence, or null.
+ * @param rowId The row id of the base recurrence.
+ * @return the relevant entries.
+ */
+ private Cursor getRelevantRecurrenceEntries(String recurrenceSyncId, long rowId) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ qb.setTables(CalendarDatabaseHelper.Views.EVENTS);
+ qb.setProjectionMap(sEventsProjectionMap);
+ String selectionArgs[];
+ if (recurrenceSyncId == null) {
+ String where = "_id =?";
+ qb.appendWhere(where);
+ selectionArgs = new String[] {String.valueOf(rowId)};
+ } else {
+ String where = "_sync_id = ? OR originalEvent = ?";
+ qb.appendWhere(where);
+ selectionArgs = new String[] {recurrenceSyncId, recurrenceSyncId};
+ }
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Retrieving events to expand: " + qb.toString());
+ }
+
+ return qb.query(mDb, EXPAND_COLUMNS, null /* selection */, selectionArgs,
+ null /* groupBy */, null /* having */, null /* sortOrder */);
+ }
+
+ /**
+ * Do incremental Instances update of a recurrence or recurrence exception.
+ *
+ * This method does performInstanceExpansion on just the modified recurrence,
+ * to avoid the overhead of recomputing the entire instance table.
+ *
+ * @param values The new values of the event.
+ * @param rowId The database row id of the event.
+ * @param db The database
+ */
+ private void updateRecurrenceInstancesLocked(ContentValues values,
+ long rowId,
+ SQLiteDatabase db) {
+ MetaData.Fields fields = mMetaData.getFieldsLocked();
+ String originalEvent = values.getAsString(Events.ORIGINAL_EVENT);
+ String recurrenceSyncId = null;
+ if (originalEvent != null) {
+ recurrenceSyncId = originalEvent;
+ } else {
+ // Get the recurrence's sync id from the database
+ recurrenceSyncId = DatabaseUtils.stringForQuery(db, "SELECT _sync_id FROM Events"
+ + " WHERE _id=?", new String[] {String.valueOf(rowId)});
+ }
+ // recurrenceSyncId is the _sync_id of the underlying recurrence
+ // If the recurrence hasn't gone to the server, it will be null.
+
+ // Need to clear out old instances
+ if (recurrenceSyncId == null) {
+ // Creating updating a recurrence that hasn't gone to the server.
+ // Need to delete based on row id
+ String where = "_id IN (SELECT Instances._id as _id"
+ + " FROM Instances INNER JOIN Events"
+ + " ON (Events._id = Instances.event_id)"
+ + " WHERE Events._id =?)";
+ db.delete("Instances", where, new String[]{"" + rowId});
+ } else {
+ // Creating or modifying a recurrence or exception.
+ // Delete instances for recurrence (_sync_id = recurrenceSyncId)
+ // and all exceptions (originalEvent = recurrenceSyncId)
+ String where = "_id IN (SELECT Instances._id as _id"
+ + " FROM Instances INNER JOIN Events"
+ + " ON (Events._id = Instances.event_id)"
+ + " WHERE Events._sync_id =?"
+ + " OR Events.originalEvent =?)";
+ db.delete("Instances", where, new String[]{recurrenceSyncId, recurrenceSyncId});
+ }
+
+ // Now do instance expansion
+ Cursor entries = getRelevantRecurrenceEntries(recurrenceSyncId, rowId);
+ try {
+ performInstanceExpansion(fields.minInstance, fields.maxInstance, fields.timezone,
+ entries);
+ } finally {
+ if (entries != null) {
+ entries.close();
+ }
+ }
+
+ // Clear busy bits (is this still needed?)
+ mMetaData.writeLocked(fields.timezone, fields.minInstance, fields.maxInstance);
+ }
+
+ long calculateLastDate(ContentValues values)
+ throws DateException {
+ // Allow updates to some event fields like the title or hasAlarm
+ // without requiring DTSTART.
+ if (!values.containsKey(Events.DTSTART)) {
+ if (values.containsKey(Events.DTEND) || values.containsKey(Events.RRULE)
+ || values.containsKey(Events.DURATION)
+ || values.containsKey(Events.EVENT_TIMEZONE)
+ || values.containsKey(Events.RDATE)
+ || values.containsKey(Events.EXRULE)
+ || values.containsKey(Events.EXDATE)) {
+ throw new RuntimeException("DTSTART field missing from event");
+ }
+ return -1;
+ }
+ long dtstartMillis = values.getAsLong(Events.DTSTART);
+ long lastMillis = -1;
+
+ // Can we use dtend with a repeating event? What does that even
+ // mean?
+ // NOTE: if the repeating event has a dtend, we convert it to a
+ // duration during event processing, so this situation should not
+ // occur.
+ Long dtEnd = values.getAsLong(Events.DTEND);
+ if (dtEnd != null) {
+ lastMillis = dtEnd;
+ } else {
+ // find out how long it is
+ Duration duration = new Duration();
+ String durationStr = values.getAsString(Events.DURATION);
+ if (durationStr != null) {
+ duration.parse(durationStr);
+ }
+
+ RecurrenceSet recur = new RecurrenceSet(values);
+
+ if (recur.hasRecurrence()) {
+ // the event is repeating, so find the last date it
+ // could appear on
+
+ String tz = values.getAsString(Events.EVENT_TIMEZONE);
+
+ if (TextUtils.isEmpty(tz)) {
+ // floating timezone
+ tz = Time.TIMEZONE_UTC;
+ }
+ Time dtstartLocal = new Time(tz);
+
+ dtstartLocal.set(dtstartMillis);
+
+ RecurrenceProcessor rp = new RecurrenceProcessor();
+ lastMillis = rp.getLastOccurence(dtstartLocal, recur);
+ if (lastMillis == -1) {
+ return lastMillis; // -1
+ }
+ } else {
+ // the event is not repeating, just use dtstartMillis
+ lastMillis = dtstartMillis;
+ }
+
+ // that was the beginning of the event. this is the end.
+ lastMillis = duration.addTo(lastMillis);
+ }
+ return lastMillis;
+ }
+
+ private ContentValues updateContentValuesFromEvent(ContentValues initialValues) {
+ try {
+ ContentValues values = new ContentValues(initialValues);
+
+ long last = calculateLastDate(values);
+ if (last != -1) {
+ values.put(Events.LAST_DATE, last);
+ }
+
+ return values;
+ } catch (DateException e) {
+ // don't add it if there was an error
+ Log.w(TAG, "Could not calculate last date.", e);
+ return null;
+ }
+ }
+
+ private void updateEventRawTimesLocked(long eventId, ContentValues values) {
+ ContentValues rawValues = new ContentValues();
+
+ rawValues.put("event_id", eventId);
+
+ String timezone = values.getAsString(Events.EVENT_TIMEZONE);
+
+ boolean allDay = false;
+ Integer allDayInteger = values.getAsInteger(Events.ALL_DAY);
+ if (allDayInteger != null) {
+ allDay = allDayInteger != 0;
+ }
+
+ if (allDay || TextUtils.isEmpty(timezone)) {
+ // floating timezone
+ timezone = Time.TIMEZONE_UTC;
+ }
+
+ Time time = new Time(timezone);
+ time.allDay = allDay;
+ Long dtstartMillis = values.getAsLong(Events.DTSTART);
+ if (dtstartMillis != null) {
+ time.set(dtstartMillis);
+ rawValues.put("dtstart2445", time.format2445());
+ }
+
+ Long dtendMillis = values.getAsLong(Events.DTEND);
+ if (dtendMillis != null) {
+ time.set(dtendMillis);
+ rawValues.put("dtend2445", time.format2445());
+ }
+
+ Long originalInstanceMillis = values.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ if (originalInstanceMillis != null) {
+ // This is a recurrence exception so we need to get the all-day
+ // status of the original recurring event in order to format the
+ // date correctly.
+ allDayInteger = values.getAsInteger(Events.ORIGINAL_ALL_DAY);
+ if (allDayInteger != null) {
+ time.allDay = allDayInteger != 0;
+ }
+ time.set(originalInstanceMillis);
+ rawValues.put("originalInstanceTime2445", time.format2445());
+ }
+
+ Long lastDateMillis = values.getAsLong(Events.LAST_DATE);
+ if (lastDateMillis != null) {
+ time.allDay = allDay;
+ time.set(lastDateMillis);
+ rawValues.put("lastDate2445", time.format2445());
+ }
+
+ mDbHelper.eventsRawTimesReplace(rawValues);
+ }
+
+ @Override
+ protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "deleteInTransaction: " + uri);
+ }
+ final boolean callerIsSyncAdapter =
+ readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case SYNCSTATE:
+ return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
+
+ case SYNCSTATE_ID:
+ String selectionWithId =
+ (BaseColumns._ID + "=" + ContentUris.parseId(uri) + " ")
+ + (selection == null ? "" : " AND (" + selection + ")");
+ return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
+
+ case EVENTS:
+ {
+ int result = 0;
+ selection = appendAccountToSelection(uri, selection);
+
+ // Query this event to get the ids to delete.
+ Cursor cursor = mDb.query("Events", ID_ONLY_PROJECTION,
+ selection, selectionArgs, null /* groupBy */,
+ null /* having */, null /* sortOrder */);
+ try {
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ result += deleteEventInternal(id, callerIsSyncAdapter);
+ }
+ } finally {
+ cursor.close();
+ cursor = null;
+ }
+ return result;
+ }
+ case EVENTS_ID:
+ {
+ long id = ContentUris.parseId(uri);
+ if (selection != null) {
+ throw new UnsupportedOperationException("CalendarProvider2 "
+ + "doesn't support selection based deletion for type "
+ + match);
+ }
+ return deleteEventInternal(id, callerIsSyncAdapter);
+ }
+ case ATTENDEES:
+ {
+ if (callerIsSyncAdapter) {
+ return mDb.delete("Attendees", selection, selectionArgs);
+ } else {
+ return deleteFromTable("Attendees", uri, selection, selectionArgs);
+ }
+ }
+ case ATTENDEES_ID:
+ {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ return mDb.delete("Attendees", "_id=?", new String[] {String.valueOf(id)});
+ } else {
+ return deleteFromTable("Attendees", uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+ }
+ case REMINDERS:
+ {
+ if (callerIsSyncAdapter) {
+ return mDb.delete("Reminders", selection, selectionArgs);
+ } else {
+ return deleteFromTable("Reminders", uri, selection, selectionArgs);
+ }
+ }
+ case REMINDERS_ID:
+ {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ return mDb.delete("Reminders", "_id=?", new String[] {String.valueOf(id)});
+ } else {
+ return deleteFromTable("Reminders", uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+ }
+ case EXTENDED_PROPERTIES:
+ {
+ if (callerIsSyncAdapter) {
+ return mDb.delete("ExtendedProperties", selection, selectionArgs);
+ } else {
+ return deleteFromTable("ExtendedProperties", uri, selection, selectionArgs);
+ }
+ }
+ case EXTENDED_PROPERTIES_ID:
+ {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ return mDb.delete("ExtendedProperties", "_id=?",
+ new String[] {String.valueOf(id)});
+ } else {
+ return deleteFromTable("ExtendedProperties", uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+ }
+ case CALENDAR_ALERTS:
+ {
+ if (callerIsSyncAdapter) {
+ return mDb.delete("CalendarAlerts", selection, selectionArgs);
+ } else {
+ return deleteFromTable("CalendarAlerts", uri, selection, selectionArgs);
+ }
+ }
+ case CALENDAR_ALERTS_ID:
+ {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ // Note: dirty bit is not set for Alerts because it is not synced.
+ // It is generated from Reminders, which is synced.
+ long id = ContentUris.parseId(uri);
+ return mDb.delete("CalendarAlerts", "_id=?", new String[] {String.valueOf(id)});
+ }
+ case DELETED_EVENTS:
+ throw new UnsupportedOperationException("Cannot delete that URL: " + uri);
+ case CALENDARS_ID:
+ StringBuilder selectionSb = new StringBuilder("_id=");
+ selectionSb.append(uri.getPathSegments().get(1));
+ if (!TextUtils.isEmpty(selection)) {
+ selectionSb.append(" AND (");
+ selectionSb.append(selection);
+ selectionSb.append(')');
+ }
+ selection = selectionSb.toString();
+ // fall through to CALENDARS for the actual delete
+ case CALENDARS:
+ selection = appendAccountToSelection(uri, selection);
+ return deleteMatchingCalendars(selection); // TODO: handle in sync adapter
+ case INSTANCES:
+ case INSTANCES_BY_DAY:
+ case EVENT_DAYS:
+ throw new UnsupportedOperationException("Cannot delete that URL");
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+ }
+
+ private int deleteEventInternal(long id, boolean callerIsSyncAdapter) {
+ int result = 0;
+
+ // Query this event to get the fields needed for deleting.
+ Cursor cursor = mDb.query("Events", EVENTS_PROJECTION,
+ "_id=?", new String[] {String.valueOf(id)},
+ null /* groupBy */,
+ null /* having */, null /* sortOrder */);
+ try {
+ if (cursor.moveToNext()) {
+ result = 1;
+ String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
+ if (!TextUtils.isEmpty(syncId)) {
+
+ // TODO: we may also want to delete exception
+ // events for this event (in case this was a
+ // recurring event). We can do that with the
+ // following code:
+ // mDb.delete("Events", "originalEvent=?", new String[] {syncId});
+ }
+
+ // If this was a recurring event or a recurrence
+ // exception, then force a recalculation of the
+ // instances.
+ String rrule = cursor.getString(EVENTS_RRULE_INDEX);
+ String rdate = cursor.getString(EVENTS_RDATE_INDEX);
+ String origEvent = cursor.getString(EVENTS_ORIGINAL_EVENT_INDEX);
+ if (!TextUtils.isEmpty(rrule) || !TextUtils.isEmpty(rdate)
+ || !TextUtils.isEmpty(origEvent)) {
+ mMetaData.clearInstanceRange();
+ }
+
+ if (callerIsSyncAdapter) {
+ mDb.delete("Events", "_id=?", new String[] {String.valueOf(id)});
+ } else {
+ ContentValues values = new ContentValues();
+ values.put(Events.DELETED, 1);
+ values.put(Events._SYNC_DIRTY, 1);
+ mDb.update("Events", values, "_id=?", new String[] {String.valueOf(id)});
+ }
+ }
+ } finally {
+ cursor.close();
+ cursor = null;
+ }
+
+ scheduleNextAlarm(false /* do not remove alarms */);
+ triggerAppWidgetUpdate(-1);
+
+ String selectionArgs[] = new String[] {String.valueOf(id)};
+ mDb.delete("Instances", "event_id=?", selectionArgs);
+ mDb.delete("EventsRawTimes", "event_id=?", selectionArgs);
+ mDb.delete("Attendees", "event_id=?", selectionArgs);
+ mDb.delete("Reminders", "event_id=?", selectionArgs);
+ mDb.delete("CalendarAlerts", "event_id=?", selectionArgs);
+ mDb.delete("ExtendedProperties", "event_id=?", selectionArgs);
+ return result;
+ }
+
+ /**
+ * Delete rows from a table and mark corresponding events as dirty.
+ * @param table The table to delete from
+ * @param uri The URI specifying the rows
+ * @param selection for the query
+ * @param selectionArgs for the query
+ */
+ private int deleteFromTable(String table, Uri uri, String selection, String[] selectionArgs) {
+ // Note that the query will return data according to the access restrictions,
+ // so we don't need to worry about deleting data we don't have permission to read.
+ Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
+ ContentValues values = new ContentValues();
+ values.put(Events._SYNC_DIRTY, "1");
+ int count = 0;
+ try {
+ while(c.moveToNext()) {
+ long id = c.getLong(ID_INDEX);
+ long event_id = c.getLong(EVENT_ID_INDEX);
+ mDb.delete(table, "_id=?", new String[] {String.valueOf(id)});
+ mDb.update("Events", values, "_id=?", new String[] {String.valueOf(event_id)});
+ count++;
+ }
+ } finally {
+ c.close();
+ }
+ return count;
+ }
+
+ /**
+ * Update rows in a table and mark corresponding events as dirty.
+ * @param table The table to delete from
+ * @param values The values to update
+ * @param uri The URI specifying the rows
+ * @param selection for the query
+ * @param selectionArgs for the query
+ */
+ private int updateInTable(String table, ContentValues values, Uri uri, String selection,
+ String[] selectionArgs) {
+ // Note that the query will return data according to the access restrictions,
+ // so we don't need to worry about deleting data we don't have permission to read.
+ Cursor c = query(uri, ID_PROJECTION, selection, selectionArgs, null);
+ ContentValues dirtyValues = new ContentValues();
+ dirtyValues.put(Events._SYNC_DIRTY, "1");
+ int count = 0;
+ try {
+ while(c.moveToNext()) {
+ long id = c.getLong(ID_INDEX);
+ long event_id = c.getLong(EVENT_ID_INDEX);
+ mDb.update(table, values, "_id=?", new String[] {String.valueOf(id)});
+ mDb.update("Events", dirtyValues, "_id=?", new String[] {String.valueOf(event_id)});
+ count++;
+ }
+ } finally {
+ c.close();
+ }
+ return count;
+ }
+
+ private int deleteMatchingCalendars(String where) {
+ // query to find all the calendars that match, for each
+ // - delete calendar subscription
+ // - delete calendar
+
+ int numDeleted = 0;
+ Cursor c = mDb.query("Calendars", sCalendarsIdProjection, where,
+ null /* selectionArgs */, null /* groupBy */,
+ null /* having */, null /* sortOrder */);
+ if (c == null) {
+ return 0;
+ }
+ try {
+ while (c.moveToNext()) {
+ long id = c.getLong(CALENDARS_INDEX_ID);
+ modifyCalendarSubscription(id, false /* not selected */);
+ c.deleteRow();
+ numDeleted++;
+ }
+ } finally {
+ c.close();
+ }
+ return numDeleted;
+ }
+
+ // TODO: call calculateLastDate()!
+ @Override
+ protected int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "updateInTransaction: " + uri);
+ }
+
+ int count = 0;
+
+ final int match = sUriMatcher.match(uri);
+
+ final boolean callerIsSyncAdapter =
+ readBooleanQueryParameter(uri, Calendar.CALLER_IS_SYNCADAPTER, false);
+
+ // TODO: remove this restriction
+ if (!TextUtils.isEmpty(selection) && match != CALENDAR_ALERTS && match != EVENTS) {
+ throw new IllegalArgumentException(
+ "WHERE based updates not supported");
+ }
+ switch (match) {
+ case SYNCSTATE:
+ return mDbHelper.getSyncState().update(mDb, values,
+ appendAccountToSelection(uri, selection), selectionArgs);
+
+ case SYNCSTATE_ID: {
+ selection = appendAccountToSelection(uri, selection);
+ String selectionWithId =
+ (BaseColumns._ID + "=" + ContentUris.parseId(uri) + " ")
+ + (selection == null ? "" : " AND (" + selection + ")");
+ return mDbHelper.getSyncState().update(mDb, values,
+ selectionWithId, selectionArgs);
+ }
+
+ case CALENDARS_ID:
+ {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ long id = ContentUris.parseId(uri);
+ Integer syncEvents = values.getAsInteger(Calendars.SYNC_EVENTS);
+ if (syncEvents != null) {
+ modifyCalendarSubscription(id, syncEvents == 1);
+ }
+
+ int result = mDb.update("Calendars", values, "_id=?",
+ new String[] {String.valueOf(id)});
+
+ return result;
+ }
+ case EVENTS:
+ case EVENTS_ID:
+ {
+ long id = 0;
+ if (match == EVENTS_ID) {
+ id = ContentUris.parseId(uri);
+ } else if (callerIsSyncAdapter) {
+ if (selection != null && selection.startsWith("_id=")) {
+ // The ContentProviderOperation generates an _id=n string instead of
+ // adding the id to the URL, so parse that out here.
+ id = Long.parseLong(selection.substring(4));
+ } else {
+ // Sync adapter Events operation affects just Events table, not associated
+ // tables.
+ return mDb.update("Events", values, selection, selectionArgs);
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+ if (!callerIsSyncAdapter) {
+ values.put(Events._SYNC_DIRTY, 1);
+ }
+ // Disallow updating the attendee status in the Events
+ // table. In the future, we could support this but we
+ // would have to query and update the attendees table
+ // to keep the values consistent.
+ if (values.containsKey(Events.SELF_ATTENDEE_STATUS)) {
+ throw new IllegalArgumentException("Updating "
+ + Events.SELF_ATTENDEE_STATUS
+ + " in Events table is not allowed.");
+ }
+
+ // TODO: should we allow this?
+ if (values.containsKey(Events.HTML_URI) && !callerIsSyncAdapter) {
+ throw new IllegalArgumentException("Updating "
+ + Events.HTML_URI
+ + " in Events table is not allowed.");
+ }
+
+ ContentValues updatedValues = updateContentValuesFromEvent(values);
+ if (updatedValues == null) {
+ Log.w(TAG, "Could not update event.");
+ return 0;
+ }
+
+ int result = mDb.update("Events", updatedValues, "_id=?",
+ new String[] {String.valueOf(id)});
+ if (result > 0) {
+ updateEventRawTimesLocked(id, updatedValues);
+ updateInstancesLocked(updatedValues, id, false /* not a new event */, mDb);
+
+ if (values.containsKey(Events.DTSTART)) {
+ // The start time of the event changed, so run the
+ // event alarm scheduler.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "updateInternal() changing event");
+ }
+ scheduleNextAlarm(false /* do not remove alarms */);
+ triggerAppWidgetUpdate(id);
+ }
+ }
+ return result;
+ }
+ case ATTENDEES_ID: {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ // Copy the attendee status value to the Events table.
+ updateEventAttendeeStatus(mDb, values);
+
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ return mDb.update("Attendees", values, "_id=?",
+ new String[] {String.valueOf(id)});
+ } else {
+ return updateInTable("Attendees", values, uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+ }
+ case CALENDAR_ALERTS_ID: {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ // Note: dirty bit is not set for Alerts because it is not synced.
+ // It is generated from Reminders, which is synced.
+ long id = ContentUris.parseId(uri);
+ return mDb.update("CalendarAlerts", values, "_id=?",
+ new String[] {String.valueOf(id)});
+ }
+ case CALENDAR_ALERTS: {
+ // Note: dirty bit is not set for Alerts because it is not synced.
+ // It is generated from Reminders, which is synced.
+ return mDb.update("CalendarAlerts", values, selection, selectionArgs);
+ }
+ case REMINDERS_ID: {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ count = mDb.update("Reminders", values, "_id=?",
+ new String[] {String.valueOf(id)});
+ } else {
+ count = updateInTable("Reminders", values, uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+
+ // Reschedule the event alarms because the
+ // "minutes" field may have changed.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "updateInternal() changing reminder");
+ }
+ scheduleNextAlarm(false /* do not remove alarms */);
+ return count;
+ }
+ case EXTENDED_PROPERTIES_ID: {
+ if (selection != null) {
+ throw new UnsupportedOperationException("Selection not permitted for " + uri);
+ }
+ if (callerIsSyncAdapter) {
+ long id = ContentUris.parseId(uri);
+ return mDb.update("ExtendedProperties", values, "_id=?",
+ new String[] {String.valueOf(id)});
+ } else {
+ return updateInTable("ExtendedProperties", values, uri, null /* selection */,
+ null /* selectionArgs */);
+ }
+ }
+ // TODO: replace the SCHEDULE_ALARM private URIs with a
+ // service
+ case SCHEDULE_ALARM: {
+ scheduleNextAlarm(false);
+ return 0;
+ }
+ case SCHEDULE_ALARM_REMOVE: {
+ scheduleNextAlarm(true);
+ return 0;
+ }
+
+ default:
+ throw new IllegalArgumentException("Unknown URL " + uri);
+ }
+ }
+
+ private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
+ final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName)) {
+ qb.appendWhere(Calendar.Calendars._SYNC_ACCOUNT + "="
+ + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(accountType));
+ } else {
+ qb.appendWhere("1"); // I.e. always true
+ }
+ }
+
+ private String appendAccountToSelection(Uri uri, String selection) {
+ final String accountName = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, Calendar.EventsEntity.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName)) {
+ StringBuilder selectionSb = new StringBuilder(Calendar.Calendars._SYNC_ACCOUNT + "="
+ + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(accountType));
+ if (!TextUtils.isEmpty(selection)) {
+ selectionSb.append(" AND (");
+ selectionSb.append(selection);
+ selectionSb.append(')');
+ }
+ return selectionSb.toString();
+ } else {
+ return selection;
+ }
+ }
+
+ private void modifyCalendarSubscription(long id, boolean syncEvents) {
+ // get the account, url, and current selected state
+ // for this calendar.
+ Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
+ new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
+ Calendars.URL, Calendars.SYNC_EVENTS},
+ null /* selection */,
+ null /* selectionArgs */,
+ null /* sort */);
+
+ Account account = null;
+ String calendarUrl = null;
+ boolean oldSyncEvents = false;
+ if (cursor != null && cursor.moveToFirst()) {
+ try {
+ final String accountName = cursor.getString(0);
+ final String accountType = cursor.getString(1);
+ account = new Account(accountName, accountType);
+ calendarUrl = cursor.getString(2);
+ oldSyncEvents = (cursor.getInt(3) != 0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ if (account == null || TextUtils.isEmpty(calendarUrl)) {
+ // should not happen?
+ Log.w(TAG, "Cannot update subscription because account "
+ + "or calendar url empty -- should not happen.");
+ return;
+ }
+
+ if (oldSyncEvents == syncEvents) {
+ // nothing to do
+ return;
+ }
+
+ // If we are no longer syncing a calendar then make sure that the
+ // old calendar sync data is cleared. Then if we later add this
+ // calendar back, we will sync all the events.
+ if (!syncEvents) {
+ // TODO: clear out the SyncState
+// byte[] data = readSyncDataBytes(account);
+// GDataSyncData syncData = AbstractGDataSyncAdapter.newGDataSyncDataFromBytes(data);
+// if (syncData != null) {
+// syncData.feedData.remove(calendarUrl);
+// data = AbstractGDataSyncAdapter.newBytesFromGDataSyncData(syncData);
+// writeSyncDataBytes(account, data);
+// }
+
+ // Delete all of the events in this calendar to save space.
+ // This is the closest we can come to deleting a calendar.
+ // Clients should never actually delete a calendar. That won't
+ // work. We need to keep the calendar entry in the Calendars table
+ // in order to know not to sync the events for that calendar from
+ // the server.
+ String[] args = new String[] {String.valueOf(id)};
+ mDb.delete("Events", CALENDAR_ID_SELECTION, args);
+
+ // TODO: cancel any pending/ongoing syncs for this calendar.
+
+ // TODO: there is a corner case to deal with here: namely, if
+ // we edit or delete an event on the phone and then remove
+ // (that is, stop syncing) a calendar, and if we also make a
+ // change on the server to that event at about the same time,
+ // then we will never propagate the changes from the phone to
+ // the server.
+ }
+
+ // If the calendar is not selected for syncing, then don't download
+ // events.
+ mDbHelper.scheduleSync(account, !syncEvents, calendarUrl);
+ }
+
+ // TODO: is this needed
+// @Override
+// public void onSyncStop(SyncContext context, boolean success) {
+// super.onSyncStop(context, success);
+// if (Log.isLoggable(TAG, Log.DEBUG)) {
+// Log.d(TAG, "onSyncStop() success: " + success);
+// }
+// scheduleNextAlarm(false /* do not remove alarms */);
+// triggerAppWidgetUpdate(-1);
+// }
+
+ /**
+ * Update any existing widgets with the changed events.
+ *
+ * @param changedEventId Specific event known to be changed, otherwise -1.
+ * If present, we use it to decide if an update is necessary.
+ */
+ private synchronized void triggerAppWidgetUpdate(long changedEventId) {
+ Context context = getContext();
+ if (context != null) {
+ mAppWidgetProvider.providerUpdated(context, changedEventId);
+ }
+ }
+
+ /* Retrieve and cache the alarm manager */
+ private AlarmManager getAlarmManager() {
+ synchronized(mAlarmLock) {
+ if (mAlarmManager == null) {
+ Context context = getContext();
+ if (context == null) {
+ Log.e(TAG, "getAlarmManager() cannot get Context");
+ return null;
+ }
+ Object service = context.getSystemService(Context.ALARM_SERVICE);
+ mAlarmManager = (AlarmManager) service;
+ }
+ return mAlarmManager;
+ }
+ }
+
+ void scheduleNextAlarmCheck(long triggerTime) {
+ AlarmManager manager = getAlarmManager();
+ if (manager == null) {
+ Log.e(TAG, "scheduleNextAlarmCheck() cannot get AlarmManager");
+ return;
+ }
+ Context context = getContext();
+ Intent intent = new Intent(CalendarReceiver.SCHEDULE);
+ intent.setClass(context, CalendarReceiver.class);
+ PendingIntent pending = PendingIntent.getBroadcast(context,
+ 0, intent, PendingIntent.FLAG_NO_CREATE);
+ if (pending != null) {
+ // Cancel any previous alarms that do the same thing.
+ manager.cancel(pending);
+ }
+ pending = PendingIntent.getBroadcast(context,
+ 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Time time = new Time();
+ time.set(triggerTime);
+ String timeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+ Log.d(TAG, "scheduleNextAlarmCheck at: " + triggerTime + timeStr);
+ }
+
+ manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pending);
+ }
+
+ /*
+ * This method runs the alarm scheduler in a background thread.
+ */
+ void scheduleNextAlarm(boolean removeAlarms) {
+ Thread thread = new AlarmScheduler(removeAlarms);
+ thread.start();
+ }
+
+ /**
+ * This method runs in a background thread and schedules an alarm for
+ * the next calendar event, if necessary.
+ */
+ private void runScheduleNextAlarm(boolean removeAlarms) {
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ if (removeAlarms) {
+ removeScheduledAlarmsLocked(db);
+ }
+ scheduleNextAlarmLocked(db);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * This method looks at the 24-hour window from now for any events that it
+ * needs to schedule. This method runs within a database transaction. It
+ * also runs in a background thread.
+ *
+ * The CalendarProvider2 keeps track of which alarms it has already scheduled
+ * to avoid scheduling them more than once and for debugging problems with
+ * alarms. It stores this knowledge in a database table called CalendarAlerts
+ * which persists across reboots. But the actual alarm list is in memory
+ * and disappears if the phone loses power. To avoid missing an alarm, we
+ * clear the entries in the CalendarAlerts table when we start up the
+ * CalendarProvider2.
+ *
+ * Scheduling an alarm multiple times is not tragic -- we filter out the
+ * extra ones when we receive them. But we still need to keep track of the
+ * scheduled alarms. The main reason is that we need to prevent multiple
+ * notifications for the same alarm (on the receive side) in case we
+ * accidentally schedule the same alarm multiple times. We don't have
+ * visibility into the system's alarm list so we can never know for sure if
+ * we have already scheduled an alarm and it's better to err on scheduling
+ * an alarm twice rather than missing an alarm. Another reason we keep
+ * track of scheduled alarms in a database table is that it makes it easy to
+ * run an SQL query to find the next reminder that we haven't scheduled.
+ *
+ * @param db the database
+ */
+ private void scheduleNextAlarmLocked(SQLiteDatabase db) {
+ AlarmManager alarmManager = getAlarmManager();
+ if (alarmManager == null) {
+ Log.e(TAG, "Failed to find the AlarmManager. Could not schedule the next alarm!");
+ return;
+ }
+
+ final long currentMillis = System.currentTimeMillis();
+ final long start = currentMillis - SCHEDULE_ALARM_SLACK;
+ final long end = start + (24 * 60 * 60 * 1000);
+ ContentResolver cr = getContext().getContentResolver();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Time time = new Time();
+ time.set(start);
+ String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+ Log.d(TAG, "runScheduleNextAlarm() start search: " + startTimeStr);
+ }
+
+ // Delete rows in CalendarAlert where the corresponding Instance or
+ // Reminder no longer exist.
+ // Also clear old alarms but keep alarms around for a while to prevent
+ // multiple alerts for the same reminder. The "clearUpToTime'
+ // should be further in the past than the point in time where
+ // we start searching for events (the "start" variable defined above).
+ String selectArg[] = new String[] {
+ Long.toString(currentMillis - CLEAR_OLD_ALARM_THRESHOLD)
+ };
+
+ int rowsDeleted =
+ db.delete(CalendarAlerts.TABLE_NAME, INVALID_CALENDARALERTS_SELECTOR, selectArg);
+
+ long nextAlarmTime = end;
+ final long tmpAlarmTime = CalendarAlerts.findNextAlarmTime(cr, currentMillis);
+ if (tmpAlarmTime != -1 && tmpAlarmTime < nextAlarmTime) {
+ nextAlarmTime = tmpAlarmTime;
+ }
+
+ // Extract events from the database sorted by alarm time. The
+ // alarm times are computed from Instances.begin (whose units
+ // are milliseconds) and Reminders.minutes (whose units are
+ // minutes).
+ //
+ // Also, ignore events whose end time is already in the past.
+ // Also, ignore events alarms that we have already scheduled.
+ //
+ // Note 1: we can add support for the case where Reminders.minutes
+ // equals -1 to mean use Calendars.minutes by adding a UNION for
+ // that case where the two halves restrict the WHERE clause on
+ // Reminders.minutes != -1 and Reminders.minutes = 1, respectively.
+ //
+ // Note 2: we have to name "myAlarmTime" different from the
+ // "alarmTime" column in CalendarAlerts because otherwise the
+ // query won't find multiple alarms for the same event.
+ String query = "SELECT begin-(minutes*60000) AS myAlarmTime,"
+ + " Instances.event_id AS eventId, begin, end,"
+ + " title, allDay, method, minutes"
+ + " FROM Instances INNER JOIN Events"
+ + " ON (Events._id = Instances.event_id)"
+ + " INNER JOIN Reminders"
+ + " ON (Instances.event_id = Reminders.event_id)"
+ + " WHERE method=" + Reminders.METHOD_ALERT
+ + " AND myAlarmTime>=" + start
+ + " AND myAlarmTime<=" + nextAlarmTime
+ + " AND end>=" + currentMillis
+ + " AND 0=(SELECT count(*) from CalendarAlerts CA"
+ + " where CA.event_id=Instances.event_id AND CA.begin=Instances.begin"
+ + " AND CA.alarmTime=myAlarmTime)"
+ + " ORDER BY myAlarmTime,begin,title";
+
+ acquireInstanceRangeLocked(start, end, false /* don't use minimum expansion windows */);
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(query, null);
+
+ final int beginIndex = cursor.getColumnIndex(Instances.BEGIN);
+ final int endIndex = cursor.getColumnIndex(Instances.END);
+ final int eventIdIndex = cursor.getColumnIndex("eventId");
+ final int alarmTimeIndex = cursor.getColumnIndex("myAlarmTime");
+ final int minutesIndex = cursor.getColumnIndex(Reminders.MINUTES);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Time time = new Time();
+ time.set(nextAlarmTime);
+ String alarmTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+ Log.d(TAG, "cursor results: " + cursor.getCount() + " nextAlarmTime: "
+ + alarmTimeStr);
+ }
+
+ while (cursor.moveToNext()) {
+ // Schedule all alarms whose alarm time is as early as any
+ // scheduled alarm. For example, if the earliest alarm is at
+ // 1pm, then we will schedule all alarms that occur at 1pm
+ // but no alarms that occur later than 1pm.
+ // Actually, we allow alarms up to a minute later to also
+ // be scheduled so that we don't have to check immediately
+ // again after an event alarm goes off.
+ final long alarmTime = cursor.getLong(alarmTimeIndex);
+ final long eventId = cursor.getLong(eventIdIndex);
+ final int minutes = cursor.getInt(minutesIndex);
+ final long startTime = cursor.getLong(beginIndex);
+ final long endTime = cursor.getLong(endIndex);
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Time time = new Time();
+ time.set(alarmTime);
+ String schedTime = time.format(" %a, %b %d, %Y %I:%M%P");
+ time.set(startTime);
+ String startTimeStr = time.format(" %a, %b %d, %Y %I:%M%P");
+
+ Log.d(TAG, " looking at id: " + eventId + " " + startTime + startTimeStr
+ + " alarm: " + alarmTime + schedTime);
+ }
+
+ if (alarmTime < nextAlarmTime) {
+ nextAlarmTime = alarmTime;
+ } else if (alarmTime >
+ nextAlarmTime + DateUtils.MINUTE_IN_MILLIS) {
+ // This event alarm (and all later ones) will be scheduled
+ // later.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "This event alarm (and all later ones) will be scheduled later");
+ }
+ break;
+ }
+
+ // Avoid an SQLiteContraintException by checking if this alarm
+ // already exists in the table.
+ if (CalendarAlerts.alarmExists(cr, eventId, startTime, alarmTime)) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ int titleIndex = cursor.getColumnIndex(Events.TITLE);
+ String title = cursor.getString(titleIndex);
+ Log.d(TAG, " alarm exists for id: " + eventId + " " + title);
+ }
+ continue;
+ }
+
+ // Insert this alarm into the CalendarAlerts table
+ Uri uri = CalendarAlerts.insert(cr, eventId, startTime,
+ endTime, alarmTime, minutes);
+ if (uri == null) {
+ Log.e(TAG, "runScheduleNextAlarm() insert into CalendarAlerts table failed");
+ continue;
+ }
+
+ CalendarAlerts.scheduleAlarm(getContext(), alarmManager, alarmTime);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // Refresh notification bar
+ if (rowsDeleted > 0) {
+ CalendarAlerts.scheduleAlarm(getContext(), alarmManager, currentMillis);
+ }
+
+ // If we scheduled an event alarm, then schedule the next alarm check
+ // for one minute past that alarm. Otherwise, if there were no
+ // event alarms scheduled, then check again in 24 hours. If a new
+ // event is inserted before the next alarm check, then this method
+ // will be run again when the new event is inserted.
+ if (nextAlarmTime != Long.MAX_VALUE) {
+ scheduleNextAlarmCheck(nextAlarmTime + DateUtils.MINUTE_IN_MILLIS);
+ } else {
+ scheduleNextAlarmCheck(currentMillis + DateUtils.DAY_IN_MILLIS);
+ }
+ }
+
+ /**
+ * Removes the entries in the CalendarAlerts table for alarms that we have
+ * scheduled but that have not fired yet. We do this to ensure that we
+ * don't miss an alarm. The CalendarAlerts table keeps track of the
+ * alarms that we have scheduled but the actual alarm list is in memory
+ * and will be cleared if the phone reboots.
+ *
+ * We don't need to remove entries that have already fired, and in fact
+ * we should not remove them because we need to display the notifications
+ * until the user dismisses them.
+ *
+ * We could remove entries that have fired and been dismissed, but we leave
+ * them around for a while because it makes it easier to debug problems.
+ * Entries that are old enough will be cleaned up later when we schedule
+ * new alarms.
+ */
+ private void removeScheduledAlarmsLocked(SQLiteDatabase db) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "removing scheduled alarms");
+ }
+ db.delete(CalendarAlerts.TABLE_NAME,
+ CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED, null /* whereArgs */);
+ }
+
+ private static String sEventsTable = "Events";
+ private static String sAttendeesTable = "Attendees";
+ private static String sRemindersTable = "Reminders";
+ private static String sCalendarAlertsTable = "CalendarAlerts";
+ private static String sExtendedPropertiesTable = "ExtendedProperties";
+
+ private static final int EVENTS = 1;
+ private static final int EVENTS_ID = 2;
+ private static final int INSTANCES = 3;
+ private static final int DELETED_EVENTS = 4;
+ private static final int CALENDARS = 5;
+ private static final int CALENDARS_ID = 6;
+ private static final int ATTENDEES = 7;
+ private static final int ATTENDEES_ID = 8;
+ private static final int REMINDERS = 9;
+ private static final int REMINDERS_ID = 10;
+ private static final int EXTENDED_PROPERTIES = 11;
+ private static final int EXTENDED_PROPERTIES_ID = 12;
+ private static final int CALENDAR_ALERTS = 13;
+ private static final int CALENDAR_ALERTS_ID = 14;
+ private static final int CALENDAR_ALERTS_BY_INSTANCE = 15;
+ private static final int INSTANCES_BY_DAY = 16;
+ private static final int SYNCSTATE = 17;
+ private static final int SYNCSTATE_ID = 18;
+ private static final int EVENT_ENTITIES = 19;
+ private static final int EVENT_ENTITIES_ID = 20;
+ private static final int EVENT_DAYS = 21;
+ private static final int SCHEDULE_ALARM = 22;
+ private static final int SCHEDULE_ALARM_REMOVE = 23;
+
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ private static final HashMap<String, String> sInstancesProjectionMap;
+ private static final HashMap<String, String> sEventsProjectionMap;
+ private static final HashMap<String, String> sEventEntitiesProjectionMap;
+ private static final HashMap<String, String> sAttendeesProjectionMap;
+ private static final HashMap<String, String> sRemindersProjectionMap;
+ private static final HashMap<String, String> sCalendarAlertsProjectionMap;
+
+ static {
+ sUriMatcher.addURI(Calendar.AUTHORITY, "instances/when/*/*", INSTANCES);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "instances/whenbyday/*/*", INSTANCES_BY_DAY);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "instances/groupbyday/*/*", EVENT_DAYS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "events", EVENTS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "events/#", EVENTS_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities", EVENT_ENTITIES);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "event_entities/#", EVENT_ENTITIES_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "calendars", CALENDARS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "calendars/#", CALENDARS_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "deleted_events", DELETED_EVENTS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "attendees", ATTENDEES);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "attendees/#", ATTENDEES_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "reminders", REMINDERS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "reminders/#", REMINDERS_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties", EXTENDED_PROPERTIES);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "extendedproperties/#", EXTENDED_PROPERTIES_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts", CALENDAR_ALERTS);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/#", CALENDAR_ALERTS_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "calendar_alerts/by_instance",
+ CALENDAR_ALERTS_BY_INSTANCE);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate", SYNCSTATE);
+ sUriMatcher.addURI(Calendar.AUTHORITY, "syncstate/#", SYNCSTATE_ID);
+ sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_PATH, SCHEDULE_ALARM);
+ sUriMatcher.addURI(Calendar.AUTHORITY, SCHEDULE_ALARM_REMOVE_PATH, SCHEDULE_ALARM_REMOVE);
+
+ sEventsProjectionMap = new HashMap<String, String>();
+ // Events columns
+ sEventsProjectionMap.put(Events.HTML_URI, "htmlUri");
+ sEventsProjectionMap.put(Events.TITLE, "title");
+ sEventsProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
+ sEventsProjectionMap.put(Events.DESCRIPTION, "description");
+ sEventsProjectionMap.put(Events.STATUS, "eventStatus");
+ sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
+ sEventsProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
+ sEventsProjectionMap.put(Events.DTSTART, "dtstart");
+ sEventsProjectionMap.put(Events.DTEND, "dtend");
+ sEventsProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
+ sEventsProjectionMap.put(Events.DURATION, "duration");
+ sEventsProjectionMap.put(Events.ALL_DAY, "allDay");
+ sEventsProjectionMap.put(Events.VISIBILITY, "visibility");
+ sEventsProjectionMap.put(Events.TRANSPARENCY, "transparency");
+ sEventsProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
+ sEventsProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
+ sEventsProjectionMap.put(Events.RRULE, "rrule");
+ sEventsProjectionMap.put(Events.RDATE, "rdate");
+ sEventsProjectionMap.put(Events.EXRULE, "exrule");
+ sEventsProjectionMap.put(Events.EXDATE, "exdate");
+ sEventsProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
+ sEventsProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
+ sEventsProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
+ sEventsProjectionMap.put(Events.LAST_DATE, "lastDate");
+ sEventsProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
+ sEventsProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
+ sEventsProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
+ sEventsProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
+ sEventsProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
+ sEventsProjectionMap.put(Events.ORGANIZER, "organizer");
+ sEventsProjectionMap.put(Events.DELETED, "deleted");
+
+ // Put the shared items into the Attendees, Reminders projection map
+ sAttendeesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+ sRemindersProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+
+ // Calendar columns
+ sEventsProjectionMap.put(Calendars.COLOR, "color");
+ sEventsProjectionMap.put(Calendars.ACCESS_LEVEL, "access_level");
+ sEventsProjectionMap.put(Calendars.SELECTED, "selected");
+ sEventsProjectionMap.put(Calendars.URL, "url");
+ sEventsProjectionMap.put(Calendars.TIMEZONE, "timezone");
+ sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, "ownerAccount");
+
+ // Put the shared items into the Instances projection map
+ // The Instances and CalendarAlerts are joined with Calendars, so the projections include
+ // the above Calendar columns.
+ sInstancesProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+ sCalendarAlertsProjectionMap = new HashMap<String, String>(sEventsProjectionMap);
+
+ sEventsProjectionMap.put(Events._ID, "_id");
+ sEventsProjectionMap.put(Events._SYNC_ID, "_sync_id");
+ sEventsProjectionMap.put(Events._SYNC_VERSION, "_sync_version");
+ sEventsProjectionMap.put(Events._SYNC_TIME, "_sync_time");
+ sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "_sync_local_id");
+ sEventsProjectionMap.put(Events._SYNC_DIRTY, "_sync_dirty");
+ sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "_sync_account");
+ sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
+ "_sync_account_type");
+
+ sEventEntitiesProjectionMap = Maps.newHashMap();
+ sEventEntitiesProjectionMap.put(Events.HTML_URI, "htmlUri");
+ sEventEntitiesProjectionMap.put(Events.TITLE, "title");
+ sEventEntitiesProjectionMap.put(Events.DESCRIPTION, "description");
+ sEventEntitiesProjectionMap.put(Events.EVENT_LOCATION, "eventLocation");
+ sEventEntitiesProjectionMap.put(Events.STATUS, "eventStatus");
+ sEventEntitiesProjectionMap.put(Events.SELF_ATTENDEE_STATUS, "selfAttendeeStatus");
+ sEventEntitiesProjectionMap.put(Events.COMMENTS_URI, "commentsUri");
+ sEventEntitiesProjectionMap.put(Events.DTSTART, "dtstart");
+ sEventEntitiesProjectionMap.put(Events.DTEND, "dtend");
+ sEventEntitiesProjectionMap.put(Events.DURATION, "duration");
+ sEventEntitiesProjectionMap.put(Events.EVENT_TIMEZONE, "eventTimezone");
+ sEventEntitiesProjectionMap.put(Events.ALL_DAY, "allDay");
+ sEventEntitiesProjectionMap.put(Events.VISIBILITY, "visibility");
+ sEventEntitiesProjectionMap.put(Events.TRANSPARENCY, "transparency");
+ sEventEntitiesProjectionMap.put(Events.HAS_ALARM, "hasAlarm");
+ sEventEntitiesProjectionMap.put(Events.HAS_EXTENDED_PROPERTIES, "hasExtendedProperties");
+ sEventEntitiesProjectionMap.put(Events.RRULE, "rrule");
+ sEventEntitiesProjectionMap.put(Events.RDATE, "rdate");
+ sEventEntitiesProjectionMap.put(Events.EXRULE, "exrule");
+ sEventEntitiesProjectionMap.put(Events.EXDATE, "exdate");
+ sEventEntitiesProjectionMap.put(Events.ORIGINAL_EVENT, "originalEvent");
+ sEventEntitiesProjectionMap.put(Events.ORIGINAL_INSTANCE_TIME, "originalInstanceTime");
+ sEventEntitiesProjectionMap.put(Events.ORIGINAL_ALL_DAY, "originalAllDay");
+ sEventEntitiesProjectionMap.put(Events.LAST_DATE, "lastDate");
+ sEventEntitiesProjectionMap.put(Events.HAS_ATTENDEE_DATA, "hasAttendeeData");
+ sEventEntitiesProjectionMap.put(Events.CALENDAR_ID, "calendar_id");
+ sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_INVITE_OTHERS, "guestsCanInviteOthers");
+ sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_MODIFY, "guestsCanModify");
+ sEventEntitiesProjectionMap.put(Events.GUESTS_CAN_SEE_GUESTS, "guestsCanSeeGuests");
+ sEventEntitiesProjectionMap.put(Events.ORGANIZER, "organizer");
+ sEventEntitiesProjectionMap.put(Events.DELETED, "deleted");
+ sEventEntitiesProjectionMap.put(Events._ID, Events._ID);
+ sEventEntitiesProjectionMap.put(Events._SYNC_ID, Events._SYNC_ID);
+ sEventEntitiesProjectionMap.put(Events._SYNC_VERSION, Events._SYNC_VERSION);
+ sEventEntitiesProjectionMap.put(Events._SYNC_DIRTY, Events._SYNC_DIRTY);
+ sEventEntitiesProjectionMap.put(Calendars.URL, "url");
+
+ // Instances columns
+ sInstancesProjectionMap.put(Instances.BEGIN, "begin");
+ sInstancesProjectionMap.put(Instances.END, "end");
+ sInstancesProjectionMap.put(Instances.EVENT_ID, "Instances.event_id AS event_id");
+ sInstancesProjectionMap.put(Instances._ID, "Instances._id AS _id");
+ sInstancesProjectionMap.put(Instances.START_DAY, "startDay");
+ sInstancesProjectionMap.put(Instances.END_DAY, "endDay");
+ sInstancesProjectionMap.put(Instances.START_MINUTE, "startMinute");
+ sInstancesProjectionMap.put(Instances.END_MINUTE, "endMinute");
+
+ // Attendees columns
+ sAttendeesProjectionMap.put(Attendees.EVENT_ID, "event_id");
+ sAttendeesProjectionMap.put(Attendees._ID, "Attendees._id AS _id");
+ sAttendeesProjectionMap.put(Attendees.ATTENDEE_NAME, "attendeeName");
+ sAttendeesProjectionMap.put(Attendees.ATTENDEE_EMAIL, "attendeeEmail");
+ sAttendeesProjectionMap.put(Attendees.ATTENDEE_STATUS, "attendeeStatus");
+ sAttendeesProjectionMap.put(Attendees.ATTENDEE_RELATIONSHIP, "attendeeRelationship");
+ sAttendeesProjectionMap.put(Attendees.ATTENDEE_TYPE, "attendeeType");
+
+ // Reminders columns
+ sRemindersProjectionMap.put(Reminders.EVENT_ID, "event_id");
+ sRemindersProjectionMap.put(Reminders._ID, "Reminders._id AS _id");
+ sRemindersProjectionMap.put(Reminders.MINUTES, "minutes");
+ sRemindersProjectionMap.put(Reminders.METHOD, "method");
+
+ // CalendarAlerts columns
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.EVENT_ID, "event_id");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts._ID, "CalendarAlerts._id AS _id");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.BEGIN, "begin");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.END, "end");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.ALARM_TIME, "alarmTime");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.STATE, "state");
+ sCalendarAlertsProjectionMap.put(CalendarAlerts.MINUTES, "minutes");
+ }
+
+ /**
+ * Make sure that there are no entries for accounts that no longer
+ * exist. We are overriding this since we need to delete from the
+ * Calendars table, which is not syncable, which has triggers that
+ * will delete from the Events and tables, which are
+ * syncable. TODO: update comment, make sure deletes don't get synced.
+ */
+ public void onAccountsUpdated(Account[] accounts) {
+ mDb = mDbHelper.getWritableDatabase();
+ if (mDb == null) return;
+
+ Map<Account, Boolean> accountHasCalendar = Maps.newHashMap();
+ Set<Account> validAccounts = Sets.newHashSet();
+ for (Account account : accounts) {
+ validAccounts.add(new Account(account.name, account.type));
+ accountHasCalendar.put(account, false);
+ }
+ ArrayList<Account> accountsToDelete = new ArrayList<Account>();
+
+ mDb.beginTransaction();
+ try {
+
+ for (String table : new String[]{"Calendars"}) {
+ // Find all the accounts the contacts DB knows about, mark the ones that aren't
+ // in the valid set for deletion.
+ Cursor c = mDb.rawQuery("SELECT DISTINCT " + CalendarDatabaseHelper.ACCOUNT_NAME
+ + ","
+ + CalendarDatabaseHelper.ACCOUNT_TYPE + " from "
+ + table, null);
+ while (c.moveToNext()) {
+ if (c.getString(0) != null && c.getString(1) != null) {
+ Account currAccount = new Account(c.getString(0), c.getString(1));
+ if (!validAccounts.contains(currAccount)) {
+ accountsToDelete.add(currAccount);
+ }
+ }
+ }
+ c.close();
+ }
+
+ for (Account account : accountsToDelete) {
+ Log.d(TAG, "removing data for removed account " + account);
+ String[] params = new String[]{account.name, account.type};
+ mDb.execSQL("DELETE FROM Calendars"
+ + " WHERE " + CalendarDatabaseHelper.ACCOUNT_NAME + "= ? AND "
+ + CalendarDatabaseHelper.ACCOUNT_TYPE
+ + "= ?", params);
+ }
+ mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ if (mCalendarClient == null) {
+ return;
+ }
+ }
+
+ /* package */ static boolean readBooleanQueryParameter(Uri uri, String name,
+ boolean defaultValue) {
+ final String flag = getQueryParameter(uri, name);
+ return flag == null
+ ? defaultValue
+ : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
+ }
+
+ // Duplicated from ContactsProvider2. TODO: a utility class for shared code
+ /**
+ * A fast re-implementation of {@link Uri#getQueryParameter}
+ */
+ /* package */ static String getQueryParameter(Uri uri, String parameter) {
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return null;
+ }
+
+ int queryLength = query.length();
+ int parameterLength = parameter.length();
+
+ String value;
+ int index = 0;
+ while (true) {
+ index = query.indexOf(parameter, index);
+ if (index == -1) {
+ return null;
+ }
+
+ index += parameterLength;
+
+ if (queryLength == index) {
+ return null;
+ }
+
+ if (query.charAt(index) == '=') {
+ index++;
+ break;
+ }
+ }
+
+ int ampIndex = query.indexOf('&', index);
+ if (ampIndex == -1) {
+ value = query.substring(index);
+ } else {
+ value = query.substring(index, ampIndex);
+ }
+
+ return Uri.decode(value);
+ }
+
+ /**
+ * Inserts an argument at the beginning of the selection arg list.
+ *
+ * The {@link android.database.sqlite.SQLiteQueryBuilder}'s where clause is
+ * prepended to the user's where clause (combined with 'AND') to generate
+ * the final where close, so arguments associated with the QueryBuilder are
+ * prepended before any user selection args to keep them in the right order.
+ */
+ private String[] insertSelectionArg(String[] selectionArgs, String arg) {
+ if (selectionArgs == null) {
+ return new String[] {arg};
+ } else {
+ int newLength = selectionArgs.length + 1;
+ String[] newSelectionArgs = new String[newLength];
+ newSelectionArgs[0] = arg;
+ System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
+ return newSelectionArgs;
+ }
+ }
+}
diff --git a/src/com/android/providers/calendar/CalendarReceiver.java b/src/com/android/providers/calendar/CalendarReceiver.java
index 21a1398..45aed34 100644
--- a/src/com/android/providers/calendar/CalendarReceiver.java
+++ b/src/com/android/providers/calendar/CalendarReceiver.java
@@ -17,10 +17,8 @@
package com.android.providers.calendar;
import android.content.BroadcastReceiver;
-import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.IContentProvider;
import android.content.Intent;
/**
@@ -31,22 +29,25 @@
* yet.
*/
public class CalendarReceiver extends BroadcastReceiver {
-
+
static final String SCHEDULE = "com.android.providers.calendar.SCHEDULE_ALARM";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
ContentResolver cr = context.getContentResolver();
- CalendarProvider provider;
- IContentProvider icp = cr.acquireProvider("calendar");
- provider = (CalendarProvider) ContentProvider.
- coerceToLocalContentProvider(icp);
if (action.equals(SCHEDULE)) {
- provider.scheduleNextAlarm(false /* do not remove alarms */);
+ cr.update(CalendarProvider2.SCHEDULE_ALARM_URI, null /* values */, null /* where */,
+ null /* selectionArgs */);
} else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
- provider.bootCompleted();
+ // Remove alarms from the CalendarAlerts table that have been marked
+ // as "scheduled" but not fired yet. We do this because the
+ // AlarmManagerService loses all information about alarms when the
+ // power turns off but we store the information in a database table
+ // that persists across reboots. See the documentation for
+ // scheduleNextAlarmLocked() for more information.
+ cr.update(CalendarProvider2.SCHEDULE_ALARM_REMOVE_URI,
+ null /* values */, null /* where */, null /* selectionArgs */);
}
- cr.releaseProvider(icp);
}
}
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapter.java b/src/com/android/providers/calendar/CalendarSyncAdapter.java
deleted file mode 100644
index f2456b5..0000000
--- a/src/com/android/providers/calendar/CalendarSyncAdapter.java
+++ /dev/null
@@ -1,1823 +0,0 @@
-/*
-**
-** Copyright 2006, 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,
-** See the License for the specific language governing permissions and
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** limitations under the License.
-*/
-
-package com.android.providers.calendar;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SyncContext;
-import android.content.SyncResult;
-import android.content.SyncableContentProvider;
-import android.database.Cursor;
-import android.database.CursorJoiner;
-import android.graphics.Color;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.SystemProperties;
-import android.pim.ICalendar;
-import android.pim.RecurrenceSet;
-import android.provider.Calendar;
-import android.provider.Calendar.Calendars;
-import android.provider.Calendar.Events;
-import android.provider.SubscribedFeeds;
-import android.provider.SyncConstValue;
-import android.provider.Settings;
-import android.text.TextUtils;
-import android.text.format.Time;
-import android.util.Config;
-import android.util.Log;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.googlelogin.GoogleLoginServiceConstants;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.CalendarEntry;
-import com.google.wireless.gdata.calendar.data.CalendarsFeed;
-import com.google.wireless.gdata.calendar.data.EventEntry;
-import com.google.wireless.gdata.calendar.data.EventsFeed;
-import com.google.wireless.gdata.calendar.data.Reminder;
-import com.google.wireless.gdata.calendar.data.When;
-import com.google.wireless.gdata.calendar.data.Who;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.GDataServiceClient;
-import com.google.wireless.gdata.client.HttpException;
-import com.google.wireless.gdata.client.QueryParams;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.data.Feed;
-import com.google.wireless.gdata.data.StringUtils;
-import com.google.wireless.gdata.parser.GDataParser;
-import com.google.wireless.gdata.parser.ParseException;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.Vector;
-import java.net.URLDecoder;
-
-/**
- * SyncAdapter for Google Calendar. Fetches the list of the user's calendars,
- * and for each calendar that is marked as "selected" in the web
- * interface, syncs that calendar.
- */
-public final class CalendarSyncAdapter extends AbstractGDataSyncAdapter {
-
- /* package */ static final String USER_AGENT_APP_VERSION = "Android-GData-Calendar/1.2";
-
- private static final String SELECT_BY_ACCOUNT =
- Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
- private static final String SELECT_BY_ACCOUNT_AND_FEED =
- SELECT_BY_ACCOUNT + " AND " + Calendars.URL + "=?";
-
- private static final String[] CALENDAR_KEY_COLUMNS =
- new String[]{Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, Calendars.URL};
- private static final String CALENDAR_KEY_SORT_ORDER =
- Calendars._SYNC_ACCOUNT + "," + Calendars._SYNC_ACCOUNT_TYPE + "," + Calendars.URL;
- private static final String[] FEEDS_KEY_COLUMNS =
- new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT,
- SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, SubscribedFeeds.Feeds.FEED};
- private static final String FEEDS_KEY_SORT_ORDER =
- SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE
- + ", " + SubscribedFeeds.Feeds.FEED;
-
- private static final String PRIVATE_FULL = "/private/full";
- private static final String FEEDS_SUBSTRING = "/feeds/";
- private static final String PRIVATE_FULL_SELFATTENDANCE = "/private/full-selfattendance";
-
- /** System property to enable sliding window sync **/
- private static final String USE_SLIDING_WINDOW = "sync.slidingwindows";
-
- private static final String HIDDEN_ATTENDEES_PROP =
- "com.android.providers.calendar.CalendarSyncAdapter#guests";
-
- public static class SyncInfo {
- // public String feedUrl;
- public long calendarId;
- public String calendarTimezone;
- }
-
- private static final String TAG = "Sync";
- private static final Integer sTentativeStatus = Events.STATUS_TENTATIVE;
- private static final Integer sConfirmedStatus = Events.STATUS_CONFIRMED;
- private static final Integer sCanceledStatus = Events.STATUS_CANCELED;
-
- private final CalendarClient mCalendarClient;
-
- private ContentResolver mContentResolver;
-
- private static final String[] CALENDARS_PROJECTION = new String[] {
- Calendars._ID, // 0
- Calendars.SELECTED, // 1
- Calendars._SYNC_TIME, // 2
- Calendars.URL, // 3
- Calendars.DISPLAY_NAME, // 4
- Calendars.TIMEZONE, // 5
- Calendars.SYNC_EVENTS, // 6
- Calendars.OWNER_ACCOUNT // 7
- };
-
- // Counters for sync event logging
- private static int mServerDiffs;
- private static int mRefresh;
-
- /** These are temporary until a real policy is implemented. **/
- private static final long DAY_IN_MS = 86400000;
- private static final long MONTH_IN_MS = 2592000000L; // 30 days
- private static final long YEAR_IN_MS = 31600000000L; // approximately
-
- protected CalendarSyncAdapter(Context context, SyncableContentProvider provider) {
- super(context, provider);
- mCalendarClient = new CalendarClient(
- new AndroidGDataClient(context, USER_AGENT_APP_VERSION),
- new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory()));
- }
-
- @Override
- protected Object createSyncInfo() {
- return new SyncInfo();
- }
-
- @Override
- protected Entry newEntry() {
- return new EventEntry();
- }
-
- @Override
- protected Cursor getCursorForTable(ContentProvider cp, Class entryClass) {
- if (entryClass != EventEntry.class) {
- throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
- }
- return cp.query(Calendar.Events.CONTENT_URI, null, null, null, null);
- }
-
- @Override
- protected Cursor getCursorForDeletedTable(ContentProvider cp, Class entryClass) {
- if (entryClass != EventEntry.class) {
- throw new IllegalArgumentException("unexpected entry class, " + entryClass.getName());
- }
- return cp.query(Calendar.Events.DELETED_CONTENT_URI, null, null, null, null);
- }
-
- @Override
- protected String cursorToEntry(SyncContext context, Cursor c, Entry entry,
- Object info) throws ParseException {
- EventEntry event = (EventEntry) entry;
- SyncInfo syncInfo = (SyncInfo) info;
-
- String feedUrl = c.getString(c.getColumnIndex(Calendars.URL));
-
- // update the sync info. this will be used later when we update the
- // provider with the results of sending this entry to the calendar
- // server.
- syncInfo.calendarId = c.getLong(c.getColumnIndex(Events.CALENDAR_ID));
- syncInfo.calendarTimezone =
- c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE));
- if (TextUtils.isEmpty(syncInfo.calendarTimezone)) {
- // if the event timezone is not set -- e.g., when we're creating an
- // event on the device -- we will use the timezone for the calendar.
- syncInfo.calendarTimezone =
- c.getString(c.getColumnIndex(Events.TIMEZONE));
- }
-
- // has attendees data. this is set to false if the proxy hid all of
- // the guests (see #entryToContentValues). in that case, we switch
- // to the self attendance feed for updates.
- boolean hasAttendees = c.getInt(c.getColumnIndex(Events.HAS_ATTENDEE_DATA)) != 0;
-
- // id, edit uri.
- // these may need to get rewritten to a self attendance projection,
- // if our proxy server has removed guests (if there were to many)
- String id = c.getString(c.getColumnIndex(Events._SYNC_ID));
- String editUri = c.getString(c.getColumnIndex(Events._SYNC_VERSION));
- if (!hasAttendees) {
- if (id != null) id = convertProjectionToSelfAttendance(id);
- if (editUri != null) editUri = convertProjectionToSelfAttendance(editUri);
- }
- event.setId(id);
- event.setEditUri(editUri);
-
- // status
- byte status;
- int localStatus = c.getInt(c.getColumnIndex(Events.STATUS));
- switch (localStatus) {
- case Events.STATUS_CANCELED:
- status = EventEntry.STATUS_CANCELED;
- break;
- case Events.STATUS_CONFIRMED:
- status = EventEntry.STATUS_CONFIRMED;
- break;
- case Events.STATUS_TENTATIVE:
- status = EventEntry.STATUS_TENTATIVE;
- break;
- default:
- // should not happen
- status = EventEntry.STATUS_TENTATIVE;
- break;
- }
- event.setStatus(status);
-
- // visibility
- byte visibility;
- int localVisibility = c.getInt(c.getColumnIndex(Events.VISIBILITY));
- switch (localVisibility) {
- case Events.VISIBILITY_DEFAULT:
- visibility = EventEntry.VISIBILITY_DEFAULT;
- break;
- case Events.VISIBILITY_CONFIDENTIAL:
- visibility = EventEntry.VISIBILITY_CONFIDENTIAL;
- break;
- case Events.VISIBILITY_PRIVATE:
- visibility = EventEntry.VISIBILITY_PRIVATE;
- break;
- case Events.VISIBILITY_PUBLIC:
- visibility = EventEntry.VISIBILITY_PUBLIC;
- break;
- default:
- // should not happen
- Log.e(TAG, "Unexpected value for visibility: " + localVisibility
- + "; using default visibility.");
- visibility = EventEntry.VISIBILITY_DEFAULT;
- break;
- }
- event.setVisibility(visibility);
-
- byte transparency;
- int localTransparency = c.getInt(c.getColumnIndex(Events.TRANSPARENCY));
- switch (localTransparency) {
- case Events.TRANSPARENCY_OPAQUE:
- transparency = EventEntry.TRANSPARENCY_OPAQUE;
- break;
- case Events.TRANSPARENCY_TRANSPARENT:
- transparency = EventEntry.TRANSPARENCY_TRANSPARENT;
- break;
- default:
- // should not happen
- Log.e(TAG, "Unexpected value for transparency: " + localTransparency
- + "; using opaque transparency.");
- transparency = EventEntry.TRANSPARENCY_OPAQUE;
- break;
- }
- event.setTransparency(transparency);
-
- // could set the html uri, but there's no need to, since it should not be edited.
-
- // title
- event.setTitle(c.getString(c.getColumnIndex(Events.TITLE)));
-
- // description
- event.setContent(c.getString(c.getColumnIndex(Events.DESCRIPTION)));
-
- // where
- event.setWhere(c.getString(c.getColumnIndex(Events.EVENT_LOCATION)));
-
- // attendees
- long eventId = c.getInt(c.getColumnIndex(Events._SYNC_LOCAL_ID));
- addAttendeesToEntry(eventId, event);
-
- // comment uri
- event.setCommentsUri(c.getString(c.getColumnIndexOrThrow(Events.COMMENTS_URI)));
-
- Time utc = new Time(Time.TIMEZONE_UTC);
-
- boolean allDay = c.getInt(c.getColumnIndex(Events.ALL_DAY)) != 0;
-
- String startTime = null;
- String endTime = null;
- // start time
- int dtstartColumn = c.getColumnIndex(Events.DTSTART);
- if (!c.isNull(dtstartColumn)) {
- long dtstart = c.getLong(dtstartColumn);
- utc.set(dtstart);
- startTime = utc.format3339(allDay);
- }
-
- // end time
- int dtendColumn = c.getColumnIndex(Events.DTEND);
- if (!c.isNull(dtendColumn)) {
- long dtend = c.getLong(dtendColumn);
- utc.set(dtend);
- endTime = utc.format3339(allDay);
- }
-
- When when = new When(startTime, endTime);
- event.addWhen(when);
-
- // reminders
- Integer hasReminder = c.getInt(c.getColumnIndex(Events.HAS_ALARM));
- if (hasReminder != null && hasReminder.intValue() != 0) {
- addRemindersToEntry(eventId, event);
- }
-
- // extendedProperties
- Integer hasExtendedProperties = c.getInt(c.getColumnIndex(Events.HAS_EXTENDED_PROPERTIES));
- if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
- addExtendedPropertiesToEntry(eventId, event);
- }
-
- long originalStartTime = -1;
- String originalId = c.getString(c.getColumnIndex(Events.ORIGINAL_EVENT));
- int originalStartTimeIndex = c.getColumnIndex(Events.ORIGINAL_INSTANCE_TIME);
- if (!c.isNull(originalStartTimeIndex)) {
- originalStartTime = c.getLong(originalStartTimeIndex);
- }
- if ((originalStartTime != -1) && !TextUtils.isEmpty(originalId)) {
- // We need to use the "originalAllDay" field for the original event
- // in order to format the "originalStartTime" correctly.
- boolean originalAllDay = c.getInt(c.getColumnIndex(Events.ORIGINAL_ALL_DAY)) != 0;
-
- String timezone = c.getString(c.getColumnIndex(Events.EVENT_TIMEZONE));
- if (TextUtils.isEmpty(timezone)) {
- timezone = TimeZone.getDefault().getID();
- }
- Time originalTime = new Time(timezone);
- originalTime.set(originalStartTime);
-
- utc.set(originalStartTime);
- event.setOriginalEventStartTime(utc.format3339(originalAllDay));
- event.setOriginalEventId(originalId);
- }
-
- // recurrences.
- ICalendar.Component component = new ICalendar.Component("DUMMY",
- null /* parent */);
- if (RecurrenceSet.populateComponent(c, component)) {
- addRecurrenceToEntry(component, event);
- }
-
- // For now, always want to send event notifications
- event.setSendEventNotifications(true);
-
- event.setGuestsCanInviteOthers(
- c.getInt(c.getColumnIndex(Events.GUESTS_CAN_INVITE_OTHERS)) != 0);
- event.setGuestsCanModify(
- c.getInt(c.getColumnIndex(Events.GUESTS_CAN_MODIFY)) != 0);
- event.setGuestsCanSeeGuests(
- c.getInt(c.getColumnIndex(Events.GUESTS_CAN_SEE_GUESTS)) != 0);
- event.setOrganizer(c.getString(c.getColumnIndex(Events.ORGANIZER)));
-
- // if this is a new entry, return the feed url. otherwise, return null; the edit url is
- // already in the entry.
- if (event.getEditUri() == null) {
- // we won't ever rewrite this to self attendance because this is a new event
- // (so if there are attendees, we need to use the full projection).
- return feedUrl;
- } else {
- return null;
- }
- }
-
- private String convertProjectionToSelfAttendance(String uri) {
- return uri.replace(PRIVATE_FULL, PRIVATE_FULL_SELFATTENDANCE);
- }
-
- private void addAttendeesToEntry(long eventId, EventEntry event)
- throws ParseException {
- Cursor c = getContext().getContentResolver().query(
- Calendar.Attendees.CONTENT_URI, null, "event_id=" + eventId, null, null);
-
- try {
- int nameIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_NAME);
- int emailIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_EMAIL);
- int statusIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_STATUS);
- int typeIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_TYPE);
- int relIndex = c.getColumnIndexOrThrow(Calendar.Attendees.ATTENDEE_RELATIONSHIP);
-
-
-
- while (c.moveToNext()) {
- Who who = new Who();
- who.setValue(c.getString(nameIndex));
- who.setEmail(c.getString(emailIndex));
- int status = c.getInt(statusIndex);
- switch (status) {
- case Calendar.Attendees.ATTENDEE_STATUS_NONE:
- who.setStatus(Who.STATUS_NONE);
- break;
- case Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED:
- who.setStatus(Who.STATUS_ACCEPTED);
- break;
- case Calendar.Attendees.ATTENDEE_STATUS_DECLINED:
- who.setStatus(Who.STATUS_DECLINED);
- break;
- case Calendar.Attendees.ATTENDEE_STATUS_INVITED:
- who.setStatus(Who.STATUS_INVITED);
- break;
- case Calendar.Attendees.ATTENDEE_STATUS_TENTATIVE:
- who.setStatus(Who.STATUS_TENTATIVE);
- break;
- default:
- Log.e(TAG, "Unknown attendee status: " + status);
- who.setStatus(Who.STATUS_NONE);
- break;
- }
- int type = c.getInt(typeIndex);
- switch (type) {
- case Calendar.Attendees.TYPE_NONE:
- who.setType(Who.TYPE_NONE);
- break;
- case Calendar.Attendees.TYPE_REQUIRED:
- who.setType(Who.TYPE_REQUIRED);
- break;
- case Calendar.Attendees.TYPE_OPTIONAL:
- who.setType(Who.TYPE_OPTIONAL);
- break;
- default:
- Log.e(TAG, "Unknown attendee type: " + type);
- who.setType(Who.TYPE_NONE);
- break;
- }
- int rel = c.getInt(relIndex);
- switch (rel) {
- case Calendar.Attendees.RELATIONSHIP_NONE:
- who.setRelationship(Who.RELATIONSHIP_NONE);
- break;
- case Calendar.Attendees.RELATIONSHIP_ATTENDEE:
- who.setRelationship(Who.RELATIONSHIP_ATTENDEE);
- break;
- case Calendar.Attendees.RELATIONSHIP_ORGANIZER:
- who.setRelationship(Who.RELATIONSHIP_ORGANIZER);
- break;
- case Calendar.Attendees.RELATIONSHIP_SPEAKER:
- who.setRelationship(Who.RELATIONSHIP_SPEAKER);
- break;
- case Calendar.Attendees.RELATIONSHIP_PERFORMER:
- who.setRelationship(Who.RELATIONSHIP_PERFORMER);
- break;
- default:
- Log.e(TAG, "Unknown attendee relationship: " + rel);
- who.setRelationship(Who.RELATIONSHIP_NONE);
- break;
- }
- event.addAttendee(who);
- }
- } finally {
- c.close();
- }
- }
-
- private void addRemindersToEntry(long eventId, EventEntry event)
- throws ParseException {
- Cursor c = getContext().getContentResolver().query(
- Calendar.Reminders.CONTENT_URI, null,
- "event_id=" + eventId, null, null);
-
- try {
- int methodIndex = c.getColumnIndex(Calendar.Reminders.METHOD);
- int minutesIndex = c.getColumnIndex(Calendar.Reminders.MINUTES);
-
- while (c.moveToNext()) {
- Reminder reminder = new Reminder();
- reminder.setMinutes(c.getInt(minutesIndex));
- int method = c.getInt(methodIndex);
- switch(method) {
- case Calendar.Reminders.METHOD_DEFAULT:
- reminder.setMethod(Reminder.METHOD_DEFAULT);
- break;
- case Calendar.Reminders.METHOD_ALERT:
- reminder.setMethod(Reminder.METHOD_ALERT);
- break;
- case Calendar.Reminders.METHOD_EMAIL:
- reminder.setMethod(Reminder.METHOD_EMAIL);
- break;
- case Calendar.Reminders.METHOD_SMS:
- reminder.setMethod(Reminder.METHOD_SMS);
- break;
- default:
- throw new ParseException("illegal method, " + method);
- }
- event.addReminder(reminder);
- }
- } finally {
- c.close();
- }
- }
-
- private void addExtendedPropertiesToEntry(long eventId, EventEntry event)
- throws ParseException {
- Cursor c = getContext().getContentResolver().query(
- Calendar.ExtendedProperties.CONTENT_URI, null,
- "event_id=" + eventId, null, null);
-
- try {
- int nameIndex = c.getColumnIndex(Calendar.ExtendedProperties.NAME);
- int valueIndex = c.getColumnIndex(Calendar.ExtendedProperties.VALUE);
-
- while (c.moveToNext()) {
- String name = c.getString(nameIndex);
- String value = c.getString(valueIndex);
- event.addExtendedProperty(name, value);
- }
- } finally {
- c.close();
- }
- }
-
- private void addRecurrenceToEntry(ICalendar.Component component,
- EventEntry event) {
- // serialize the component into a Google Calendar recurrence string
- // we don't serialize the entire component, since we have a dummy
- // wrapper (BEGIN:DUMMY, END:DUMMY).
- StringBuilder sb = new StringBuilder();
-
- // append the properties
- boolean first = true;
- for (String propertyName : component.getPropertyNames()) {
- for (ICalendar.Property property :
- component.getProperties(propertyName)) {
- if (first) {
- first = false;
- } else {
- sb.append("\n");
- }
- property.toString(sb);
- }
- }
-
- // append the sub-components
- List<ICalendar.Component> children = component.getComponents();
- if (children != null) {
- for (ICalendar.Component child : children) {
- if (first) {
- first = false;
- } else {
- sb.append("\n");
- }
- child.toString(sb);
- }
- }
- event.setRecurrence(sb.toString());
- }
-
- @Override
- protected void deletedCursorToEntry(SyncContext context, Cursor c, Entry entry) {
- EventEntry event = (EventEntry) entry;
- event.setId(c.getString(c.getColumnIndex(Events._SYNC_ID)));
- event.setEditUri(c.getString(c.getColumnIndex(Events._SYNC_VERSION)));
- event.setStatus(EventEntry.STATUS_CANCELED);
- }
-
- protected boolean handleAllDeletedUnavailable(GDataSyncData syncData, String feed) {
- syncData.feedData.remove(feed);
- final Account account = getAccount();
- getContext().getContentResolver().delete(Calendar.Calendars.CONTENT_URI,
- Calendar.Calendars._SYNC_ACCOUNT + "=? AND "
- + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=? AND "
- + Calendar.Calendars.URL + "=?",
- new String[]{account.name, account.type, feed});
- return true;
- }
-
- @Override
- public void onSyncStarting(SyncContext context, Account account, boolean manualSync,
- SyncResult result) {
- mContentResolver = getContext().getContentResolver();
- mServerDiffs = 0;
- mRefresh = 0;
- super.onSyncStarting(context, account, manualSync, result);
- }
-
- public boolean getIsSyncable(Account account)
- throws IOException, AuthenticatorException, OperationCanceledException {
- Account[] accounts = AccountManager.get(getContext()).getAccountsByTypeAndFeatures(
- "com.google", new String[]{"legacy_hosted_or_google"}, null, null).getResult();
- return accounts.length > 0 && accounts[0].equals(account) && super.getIsSyncable(account);
- }
-
- private void deletedEntryToContentValues(Long syncLocalId, EventEntry event,
- ContentValues values) {
- // see #deletedCursorToEntry. this deletion cannot be an exception to a recurrence (e.g.,
- // deleting an instance of a repeating event) -- new recurrence exceptions would be
- // insertions.
- values.clear();
-
- // Base sync info
- values.put(Events._SYNC_LOCAL_ID, syncLocalId);
- values.put(Events._SYNC_ID, event.getId());
- values.put(Events._SYNC_VERSION, event.getEditUri());
- }
-
- /**
- * Clear out the map and stuff an Entry into it in a format that can
- * be inserted into a content provider.
- *
- * If a date is before 1970 or past 2038, ENTRY_INVALID is returned, and DTSTART
- * is set to -1. This is due to the current 32-bit time restriction and
- * will be fixed in a future release.
- *
- * @return ENTRY_OK, ENTRY_DELETED, or ENTRY_INVALID
- */
- private int entryToContentValues(EventEntry event, Long syncLocalId,
- ContentValues map, Object info) {
- SyncInfo syncInfo = (SyncInfo) info;
-
- // There are 3 cases for parsing a date-time string:
- //
- // 1. The date-time string specifies a date and time with a time offset.
- // (The "normal" format.)
- // 2. The date-time string is just a date, used for all-day events,
- // with no time or time offset fields. (The "all-day" format.)
- // 3. The date-time string specifies a date and time, but no time
- // offset. (The "floating" format, not supported yet.)
- //
- // Case 1: Time.parse3339() converts the date-time string to UTC and
- // sets the Time.timezone to UTC. It does not matter what the initial
- // Time.timezone field was set to. The initial timezone is ignored.
- //
- // Case 2: The date-time string doesn't specify the time.
- // Time.parse3339() just sets the date but not the time (hour, minute,
- // second) fields. (The time fields should be zero, meaning midnight.)
- // This code then sets the timezone to UTC (because this is an all-day
- // event). It does not matter in this case either what the initial
- // Time.timezone field was set to.
- //
- // Case 3: This is a "floating time" (which we do not support yet).
- // In this case, it will matter what the initial Time.timezone is set
- // to. It should use UTC. If I specify a floating time of 1pm then I
- // want that event displayed at 1pm in every timezone. The easiest way
- // to support this would be store it as 1pm in UTC and mark the event
- // as "isFloating" (with a new database column). Then when displaying
- // the event, the code checks "isFloating" and just leaves the time at
- // 1pm without doing any conversion to the local timezone.
- //
- // So in all cases, it is correct to set the Time.timezone to UTC.
- Time time = new Time(Time.TIMEZONE_UTC);
-
- map.clear();
-
- // Base sync info
- map.put(Events._SYNC_ID, event.getId());
- String version = event.getEditUri();
- final Account account = getAccount();
- if (!StringUtils.isEmpty(version)) {
- // Always rewrite the edit URL to https for dasher account to avoid
- // redirection.
- map.put(Events._SYNC_VERSION, rewriteUrlforAccount(account, version));
- }
-
- // see if this is an exception to an existing event/recurrence.
- String originalId = event.getOriginalEventId();
- String originalStartTime = event.getOriginalEventStartTime();
- boolean isRecurrenceException = false;
- if (!StringUtils.isEmpty(originalId) && !StringUtils.isEmpty(originalStartTime)) {
- isRecurrenceException = true;
- time.parse3339(originalStartTime);
- map.put(Events.ORIGINAL_EVENT, originalId);
- map.put(Events.ORIGINAL_INSTANCE_TIME, time.toMillis(false /* use isDst */));
- map.put(Events.ORIGINAL_ALL_DAY, time.allDay ? 1 : 0);
- }
-
- // Event status
- byte status = event.getStatus();
- switch (status) {
- case EventEntry.STATUS_CANCELED:
- if (!isRecurrenceException) {
- return ENTRY_DELETED;
- }
- map.put(Events.STATUS, sCanceledStatus);
- break;
- case EventEntry.STATUS_TENTATIVE:
- map.put(Events.STATUS, sTentativeStatus);
- break;
- case EventEntry.STATUS_CONFIRMED:
- map.put(Events.STATUS, sConfirmedStatus);
- break;
- default:
- // should not happen
- return ENTRY_INVALID;
- }
-
- map.put(Events._SYNC_LOCAL_ID, syncLocalId);
-
- // Updated time, only needed for non-deleted items
- String updated = event.getUpdateDate();
- map.put(Events._SYNC_TIME, updated);
- map.put(Events._SYNC_DIRTY, 0);
-
- // visibility
- switch (event.getVisibility()) {
- case EventEntry.VISIBILITY_DEFAULT:
- map.put(Events.VISIBILITY, Events.VISIBILITY_DEFAULT);
- break;
- case EventEntry.VISIBILITY_CONFIDENTIAL:
- map.put(Events.VISIBILITY, Events.VISIBILITY_CONFIDENTIAL);
- break;
- case EventEntry.VISIBILITY_PRIVATE:
- map.put(Events.VISIBILITY, Events.VISIBILITY_PRIVATE);
- break;
- case EventEntry.VISIBILITY_PUBLIC:
- map.put(Events.VISIBILITY, Events.VISIBILITY_PUBLIC);
- break;
- default:
- // should not happen
- Log.e(TAG, "Unexpected visibility " + event.getVisibility());
- return ENTRY_INVALID;
- }
-
- // transparency
- switch (event.getTransparency()) {
- case EventEntry.TRANSPARENCY_OPAQUE:
- map.put(Events.TRANSPARENCY, Events.TRANSPARENCY_OPAQUE);
- break;
- case EventEntry.TRANSPARENCY_TRANSPARENT:
- map.put(Events.TRANSPARENCY, Events.TRANSPARENCY_TRANSPARENT);
- break;
- default:
- // should not happen
- Log.e(TAG, "Unexpected transparency " + event.getTransparency());
- return ENTRY_INVALID;
- }
-
- // html uri
- String htmlUri = event.getHtmlUri();
- if (!StringUtils.isEmpty(htmlUri)) {
- // TODO: convert this desktop url into a mobile one?
- // htmlUri = htmlUri.replace("/event?", "/mevent?"); // but a little more robust
- map.put(Events.HTML_URI, htmlUri);
- }
-
- // title
- String title = event.getTitle();
- if (!StringUtils.isEmpty(title)) {
- map.put(Events.TITLE, title);
- }
-
- // content
- String content = event.getContent();
- if (!StringUtils.isEmpty(content)) {
- map.put(Events.DESCRIPTION, content);
- }
-
- // where
- String where = event.getWhere();
- if (!StringUtils.isEmpty(where)) {
- map.put(Events.EVENT_LOCATION, where);
- }
-
- // Calendar ID
- map.put(Events.CALENDAR_ID, syncInfo.calendarId);
-
- // comments uri
- String commentsUri = event.getCommentsUri();
- if (commentsUri != null) {
- map.put(Events.COMMENTS_URI, commentsUri);
- }
-
- boolean timesSet = false;
-
- // see if there are any reminders for this event
- if (event.getReminders() != null) {
- // just store that we have reminders. the caller will have
- // to update the reminders table separately.
- map.put(Events.HAS_ALARM, 1);
- }
-
- boolean hasAttendeeData = true;
- // see if there are any extended properties for this event
- if (event.getExtendedProperties() != null) {
- // first, intercept the proxy's hint that it has stripped attendees
- Hashtable props = event.getExtendedProperties();
- if (props.containsKey(HIDDEN_ATTENDEES_PROP) &&
- "hidden".equals(props.get(HIDDEN_ATTENDEES_PROP))) {
- props.remove(HIDDEN_ATTENDEES_PROP);
- hasAttendeeData = false;
- }
- // just store that we have extended properties. the caller will have
- // to update the extendedproperties table separately.
- map.put(Events.HAS_EXTENDED_PROPERTIES, ((props.size() > 0) ? 1 : 0));
- }
-
- map.put(Events.HAS_ATTENDEE_DATA, hasAttendeeData ? 1 : 0);
-
- // dtstart & dtend
- When when = event.getFirstWhen();
- if (when != null) {
- String startTime = when.getStartTime();
- if (!StringUtils.isEmpty(startTime)) {
- time.parse3339(startTime);
-
- // we also stash away the event's timezone.
- // this timezone might get overwritten below, if this event is
- // a recurrence (recurrences are defined in terms of the
- // timezone of the creator of the event).
- // note that we treat all day events as occurring in the UTC timezone, so
- // an event on 05/08/2007 occurs on 05/08/2007, no matter what timezone the device
- // is in.
- // TODO: handle the "floating" timezone.
- if (time.allDay) {
- map.put(Events.ALL_DAY, 1);
- map.put(Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC);
- } else {
- map.put(Events.EVENT_TIMEZONE, syncInfo.calendarTimezone);
- }
-
- long dtstart = time.toMillis(false /* use isDst */);
- if (dtstart < 0) {
- if (Config.LOGD) {
- Log.d(TAG, "dtstart out of range: " + startTime);
- }
- map.put(Events.DTSTART, -1); // Flag to caller that date is out of range
- return ENTRY_INVALID;
- }
- map.put(Events.DTSTART, dtstart);
-
- timesSet = true;
- }
-
- String endTime = when.getEndTime();
- if (!StringUtils.isEmpty(endTime)) {
- time.parse3339(endTime);
- long dtend = time.toMillis(false /* use isDst */);
- if (dtend < 0) {
- if (Config.LOGD) {
- Log.d(TAG, "dtend out of range: " + endTime);
- }
- map.put(Events.DTSTART, -1); // Flag to caller that date is out of range
- return ENTRY_INVALID;
- }
- map.put(Events.DTEND, dtend);
- }
- }
-
- // rrule
- String recurrence = event.getRecurrence();
- if (!TextUtils.isEmpty(recurrence)) {
- ICalendar.Component recurrenceComponent =
- new ICalendar.Component("DUMMY", null /* parent */);
- ICalendar ical = null;
- try {
- ICalendar.parseComponent(recurrenceComponent, recurrence);
- } catch (ICalendar.FormatException fe) {
- if (Config.LOGD) {
- Log.d(TAG, "Unable to parse recurrence: " + recurrence);
- }
- return ENTRY_INVALID;
- }
-
- if (!RecurrenceSet.populateContentValues(recurrenceComponent, map)) {
- return ENTRY_INVALID;
- }
-
- timesSet = true;
- }
-
- if (!timesSet) {
- return ENTRY_INVALID;
- }
-
- map.put(SyncConstValue._SYNC_ACCOUNT, account.name);
- map.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.type);
-
- map.put(Events.GUESTS_CAN_INVITE_OTHERS, event.getGuestsCanInviteOthers() ? 1 : 0);
- map.put(Events.GUESTS_CAN_MODIFY, event.getGuestsCanModify() ? 1 : 0);
- map.put(Events.GUESTS_CAN_SEE_GUESTS, event.getGuestsCanSeeGuests() ? 1 : 0);
-
- // Find the organizer for this event
- String organizer = null;
- Vector attendees = event.getAttendees();
- Enumeration attendeesEnum = attendees.elements();
- while (attendeesEnum.hasMoreElements()) {
- Who who = (Who) attendeesEnum.nextElement();
- if (who.getRelationship() == Who.RELATIONSHIP_ORGANIZER) {
- organizer = who.getEmail();
- break;
- }
- }
- if (organizer != null) {
- map.put(Events.ORGANIZER, organizer);
- }
-
- return ENTRY_OK;
- }
-
- public void updateProvider(Feed feed,
- Long syncLocalId, Entry entry,
- ContentProvider provider, Object info,
- GDataSyncData.FeedData feedSyncData) throws ParseException {
- SyncInfo syncInfo = (SyncInfo) info;
- EventEntry event = (EventEntry) entry;
-
- ContentValues map = new ContentValues();
-
- // use the calendar's timezone, if provided in the feed.
- // this overwrites whatever was in the db.
- if ((feed != null) && (feed instanceof EventsFeed)) {
- EventsFeed eventsFeed = (EventsFeed) feed;
- syncInfo.calendarTimezone = eventsFeed.getTimezone();
- }
-
- if (entry.isDeleted()) {
- deletedEntryToContentValues(syncLocalId, event, map);
- if (Config.LOGV) {
- Log.v(TAG, "Deleting entry: " + map);
- }
- provider.insert(Events.DELETED_CONTENT_URI, map);
- return;
- }
-
- int entryState = entryToContentValues(event, syncLocalId, map, syncInfo);
-
- // See if event is inside the window
- // feedSyncData will be null if the phone is creating the event
- if (entryState == ENTRY_OK && (feedSyncData == null || feedSyncData.newWindowEnd == 0)) {
- // A regular sync. Accept the event if it is inside the sync window or
- // it is a recurrence exception for something inside the sync window.
-
- Long dtstart = map.getAsLong(Events.DTSTART);
- if (dtstart != null && (feedSyncData == null || dtstart < feedSyncData.windowEnd)) {
- // dstart inside window, keeping event
- } else {
- Long originalInstanceTime = map.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- if (originalInstanceTime != null &&
- (feedSyncData == null || originalInstanceTime <= feedSyncData.windowEnd)) {
- // originalInstanceTime inside the window, keeping event
- } else {
- // Rejecting event as outside window
- return;
- }
- }
- }
-
- if (entryState == ENTRY_DELETED) {
- if (Config.LOGV) {
- Log.v(TAG, "Got deleted entry from server: "
- + map);
- }
- provider.insert(Events.DELETED_CONTENT_URI, map);
- } else if (entryState == ENTRY_OK) {
- if (Config.LOGV) {
- Log.v(TAG, "Got entry from server: " + map);
- }
- Uri result = provider.insert(Events.CONTENT_URI, map);
- long rowId = ContentUris.parseId(result);
- // handle the reminders for the event
- Integer hasAlarm = map.getAsInteger(Events.HAS_ALARM);
- if (hasAlarm != null && hasAlarm == 1) {
- // reminders should not be null
- Vector alarms = event.getReminders();
- if (alarms == null) {
- Log.e(TAG, "Have an alarm but do not have any reminders "
- + "-- should not happen.");
- throw new IllegalStateException("Have an alarm but do not have any reminders");
- }
- Enumeration reminders = alarms.elements();
- while (reminders.hasMoreElements()) {
- ContentValues reminderValues = new ContentValues();
- reminderValues.put(Calendar.Reminders.EVENT_ID, rowId);
-
- Reminder reminder = (Reminder) reminders.nextElement();
- byte method = reminder.getMethod();
- switch (method) {
- case Reminder.METHOD_DEFAULT:
- reminderValues.put(Calendar.Reminders.METHOD,
- Calendar.Reminders.METHOD_DEFAULT);
- break;
- case Reminder.METHOD_ALERT:
- reminderValues.put(Calendar.Reminders.METHOD,
- Calendar.Reminders.METHOD_ALERT);
- break;
- case Reminder.METHOD_EMAIL:
- reminderValues.put(Calendar.Reminders.METHOD,
- Calendar.Reminders.METHOD_EMAIL);
- break;
- case Reminder.METHOD_SMS:
- reminderValues.put(Calendar.Reminders.METHOD,
- Calendar.Reminders.METHOD_SMS);
- break;
- default:
- // should not happen. return false? we'd have to
- // roll back the event.
- Log.e(TAG, "Unknown reminder method: " + method
- + " should not happen!");
- }
-
- int minutes = reminder.getMinutes();
- reminderValues.put(Calendar.Reminders.MINUTES,
- minutes == Reminder.MINUTES_DEFAULT ?
- Calendar.Reminders.MINUTES_DEFAULT :
- minutes);
-
- if (provider.insert(Calendar.Reminders.CONTENT_URI,
- reminderValues) == null) {
- throw new ParseException("Unable to insert reminders.");
- }
- }
- }
-
- // handle attendees for the event
- Vector attendees = event.getAttendees();
- Enumeration attendeesEnum = attendees.elements();
- while (attendeesEnum.hasMoreElements()) {
- Who who = (Who) attendeesEnum.nextElement();
- ContentValues attendeesValues = new ContentValues();
- attendeesValues.put(Calendar.Attendees.EVENT_ID, rowId);
- attendeesValues.put(Calendar.Attendees.ATTENDEE_NAME, who.getValue());
- attendeesValues.put(Calendar.Attendees.ATTENDEE_EMAIL, who.getEmail());
-
- byte status;
- switch (who.getStatus()) {
- case Who.STATUS_NONE:
- status = Calendar.Attendees.ATTENDEE_STATUS_NONE;
- break;
- case Who.STATUS_INVITED:
- status = Calendar.Attendees.ATTENDEE_STATUS_INVITED;
- break;
- case Who.STATUS_ACCEPTED:
- status = Calendar.Attendees.ATTENDEE_STATUS_ACCEPTED;
- break;
- case Who.STATUS_TENTATIVE:
- status = Calendar.Attendees.ATTENDEE_STATUS_TENTATIVE;
- break;
- case Who.STATUS_DECLINED:
- status = Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
- break;
- default:
- Log.w(TAG, "Unknown attendee status " + who.getStatus());
- status = Calendar.Attendees.ATTENDEE_STATUS_NONE;
- }
- attendeesValues.put(Calendar.Attendees.ATTENDEE_STATUS, status);
- byte rel;
- switch (who.getRelationship()) {
- case Who.RELATIONSHIP_NONE:
- rel = Calendar.Attendees.RELATIONSHIP_NONE;
- break;
- case Who.RELATIONSHIP_ORGANIZER:
- rel = Calendar.Attendees.RELATIONSHIP_ORGANIZER;
- break;
- case Who.RELATIONSHIP_ATTENDEE:
- rel = Calendar.Attendees.RELATIONSHIP_ATTENDEE;
- break;
- case Who.RELATIONSHIP_PERFORMER:
- rel = Calendar.Attendees.RELATIONSHIP_PERFORMER;
- break;
- case Who.RELATIONSHIP_SPEAKER:
- rel = Calendar.Attendees.RELATIONSHIP_SPEAKER;
- break;
- default:
- Log.w(TAG, "Unknown attendee relationship " + who.getRelationship());
- rel = Calendar.Attendees.RELATIONSHIP_NONE;
- }
-
- attendeesValues.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP, rel);
-
- byte type;
- switch (who.getType()) {
- case Who.TYPE_NONE:
- type = Calendar.Attendees.TYPE_NONE;
- break;
- case Who.TYPE_REQUIRED:
- type = Calendar.Attendees.TYPE_REQUIRED;
- break;
- case Who.TYPE_OPTIONAL:
- type = Calendar.Attendees.TYPE_OPTIONAL;
- break;
- default:
- Log.w(TAG, "Unknown attendee type " + who.getType());
- type = Calendar.Attendees.TYPE_NONE;
- }
- attendeesValues.put(Calendar.Attendees.ATTENDEE_TYPE, type);
- if (provider.insert(Calendar.Attendees.CONTENT_URI, attendeesValues) == null) {
- throw new ParseException("Unable to insert attendees.");
- }
- }
-
- // handle the extended properties for the event
- Integer hasExtendedProperties = map.getAsInteger(Events.HAS_EXTENDED_PROPERTIES);
- if (hasExtendedProperties != null && hasExtendedProperties.intValue() != 0) {
- // extended properties should not be null
- // TODO: make the extended properties a bit more OO?
- Hashtable extendedProperties = event.getExtendedProperties();
- if (extendedProperties == null) {
- Log.e(TAG, "Have extendedProperties but do not have any properties"
- + "-- should not happen.");
- throw new IllegalStateException(
- "Have extendedProperties but do not have any properties");
- }
- Enumeration propertyNames = extendedProperties.keys();
- while (propertyNames.hasMoreElements()) {
- String propertyName = (String) propertyNames.nextElement();
- String propertyValue = (String) extendedProperties.get(propertyName);
- ContentValues extendedPropertyValues = new ContentValues();
- extendedPropertyValues.put(Calendar.ExtendedProperties.EVENT_ID, rowId);
- extendedPropertyValues.put(Calendar.ExtendedProperties.NAME,
- propertyName);
- extendedPropertyValues.put(Calendar.ExtendedProperties.VALUE,
- propertyValue);
- if (provider.insert(Calendar.ExtendedProperties.CONTENT_URI,
- extendedPropertyValues) == null) {
- throw new ParseException("Unable to insert extended properties.");
- }
- }
- }
- } else {
- // If the DTSTART == -1, then the date was out of range. We don't
- // need to throw a ParseException because the user can create
- // dates on the web that we can't handle on the phone. For
- // example, events with dates before Dec 13, 1901 can be created
- // on the web but cannot be handled on the phone.
- Long dtstart = map.getAsLong(Events.DTSTART);
- if (dtstart != null && dtstart == -1) {
- return;
- }
-
- if (Config.LOGV) {
- Log.v(TAG, "Got invalid entry from server: " + map);
- }
- throw new ParseException("Got invalid entry from server: " + map);
- }
- }
-
- /**
- * Converts an old non-sliding-windows database to sliding windows
- * @param feedSyncData State of the sync.
- */
- private void upgradeToSlidingWindows(GDataSyncData.FeedData feedSyncData) {
- feedSyncData.windowEnd = getSyncWindowEnd();
- // TODO: Should prune old events
- }
-
- @Override
- public void getServerDiffs(SyncContext context,
- SyncData baseSyncData, SyncableContentProvider tempProvider,
- Bundle extras, Object baseSyncInfo, SyncResult syncResult) {
- final ContentResolver cr = getContext().getContentResolver();
- mServerDiffs++;
- final boolean syncingSingleFeed = (extras != null) && extras.containsKey("feed");
- final boolean syncingMetafeedOnly = (extras != null) && extras.containsKey("metafeedonly");
-
- if (syncingSingleFeed) {
- if (syncingMetafeedOnly) {
- Log.d(TAG, "metafeedonly and feed both set.");
- return;
- }
- StringBuilder sb = new StringBuilder();
- extrasToStringBuilder(extras, sb);
- String feedUrl = extras.getString("feed");
-
- GDataSyncData.FeedData feedSyncData = getFeedData(feedUrl, baseSyncData);
- if (feedSyncData != null && feedSyncData.windowEnd == 0) {
- upgradeToSlidingWindows(feedSyncData);
- } else if (feedSyncData == null) {
- feedSyncData = new GDataSyncData.FeedData(0, 0, false, "", 0);
- feedSyncData.windowEnd = getSyncWindowEnd();
- ((GDataSyncData) baseSyncData).feedData.put(feedUrl, feedSyncData);
- }
-
- if (extras.getBoolean("moveWindow", false)) {
- // This is a move window sync. Set the new end.
- // Setting newWindowEnd makes this a sliding window expansion sync.
- if (feedSyncData.newWindowEnd == 0) {
- feedSyncData.newWindowEnd = getSyncWindowEnd();
- }
- } else {
- if (getSyncWindowEnd() > feedSyncData.windowEnd) {
- // Schedule a move-the-window sync
-
- Bundle syncExtras = new Bundle();
- syncExtras.clear();
- syncExtras.putBoolean("moveWindow", true);
- syncExtras.putString("feed", feedUrl);
- ContentResolver.requestSync(null /* account */, Calendar.AUTHORITY, syncExtras);
- }
- }
- getServerDiffsForFeed(context, baseSyncData, tempProvider, feedUrl,
- baseSyncInfo, syncResult);
- return;
- }
-
- // At this point, either metafeed sync or poll.
- // For the poll (or metafeed sync), refresh the list of calendars.
- // we can move away from this when we move to the new allcalendars feed, which is
- // syncable. until then, we'll rely on the daily poll to keep the list of calendars
- // up to date.
-
- mRefresh++;
- context.setStatusText("Fetching list of calendars");
- fetchCalendarsFromServer();
-
- if (syncingMetafeedOnly) {
- // If not polling, nothing more to do.
- return;
- }
-
- // select the set of calendars for this account.
- final Account account = getAccount();
- final String[] accountSelectionArgs = new String[]{account.name, account.type};
- Cursor cursor = cr.query(Calendar.Calendars.CONTENT_URI,
- CALENDARS_PROJECTION, SELECT_BY_ACCOUNT,
- accountSelectionArgs, null /* sort order */);
-
- Bundle syncExtras = new Bundle();
-
- try {
- while (cursor.moveToNext()) {
- boolean syncEvents = (cursor.getInt(6) == 1);
- String feedUrl = cursor.getString(3);
-
- if (!syncEvents) {
- continue;
- }
-
- // schedule syncs for each of these feeds.
- syncExtras.clear();
- syncExtras.putAll(extras);
- syncExtras.putString("feed", feedUrl);
- ContentResolver.requestSync(account,
- Calendar.Calendars.CONTENT_URI.getAuthority(), syncExtras);
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Gets end of the sliding sync window.
- *
- * @return end of window in ms
- */
- private long getSyncWindowEnd() {
- // How many days in the future the window extends (e.g. 1 year). 0 for no sliding window.
- long window = Settings.Gservices.getLong(getContext().getContentResolver(),
- Settings.Gservices.GOOGLE_CALENDAR_SYNC_WINDOW_DAYS, 0);
- if (window > 0) {
- // How often to advance the window (e.g. 30 days)
- long advanceInterval = Settings.Gservices.getLong(getContext().getContentResolver(),
- Settings.Gservices.GOOGLE_CALENDAR_SYNC_WINDOW_UPDATE_DAYS, 30) * DAY_IN_MS;
- if (advanceInterval > 0) {
- // endOfWindow is the proposed end of the sliding window (e.g. 1 year out)
- long endOfWindow = System.currentTimeMillis() + window * DAY_IN_MS;
- // We don't want the end of the window to advance smoothly or else we would
- // be constantly doing syncs to update the window. We "snap" the window to
- // a multiple of advanceInterval so the end of the window will only advance
- // every e.g. 30 days. By dividing and multiplying by advanceInterval, the
- // window is truncated down to a multiple of advanceInterval. This provides
- // the "snap" action.
- return (endOfWindow / advanceInterval) * advanceInterval;
- }
- }
- return Long.MAX_VALUE;
- }
-
- private void getServerDiffsForFeed(SyncContext context, SyncData baseSyncData,
- SyncableContentProvider tempProvider,
- String feed, Object baseSyncInfo, SyncResult syncResult) {
- final SyncInfo syncInfo = (SyncInfo) baseSyncInfo;
- final GDataSyncData syncData = (GDataSyncData) baseSyncData;
-
- final Account account = getAccount();
- Cursor cursor = getContext().getContentResolver().query(Calendar.Calendars.CONTENT_URI,
- CALENDARS_PROJECTION, SELECT_BY_ACCOUNT_AND_FEED,
- new String[] { account.name, account.type, feed }, null /* sort order */);
-
- ContentValues map = new ContentValues();
- int maxResults = getMaxEntriesPerSync();
-
- try {
- if (!cursor.moveToFirst()) {
- return;
- }
- // TODO: refactor all of this, so we don't have to rely on
- // member variables getting updated here in order for the
- // base class hooks to work.
-
- syncInfo.calendarId = cursor.getLong(0);
- boolean syncEvents = (cursor.getInt(6) == 1);
- long syncTime = cursor.getLong(2);
- String feedUrl = cursor.getString(3);
- String name = cursor.getString(4);
- String origCalendarTimezone =
- syncInfo.calendarTimezone = cursor.getString(5);
-
- if (!syncEvents) {
- // should not happen. non-syncable feeds should not be scheduled for syncs nor
- // should they get tickled.
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Ignoring sync request for non-syncable feed.");
- }
- return;
- }
-
- context.setStatusText("Syncing " + name);
-
- // call the superclass implementation to sync the current
- // calendar from the server.
- getServerDiffsImpl(context, tempProvider, getFeedEntryClass(), feedUrl, syncInfo,
- maxResults, syncData, syncResult);
- if (mSyncCanceled || syncResult.hasError()) {
- return;
- }
-
- // update the timezone for this calendar if it changed
- if (!TextUtils.equals(syncInfo.calendarTimezone,
- origCalendarTimezone)) {
- map.clear();
- map.put(Calendars.TIMEZONE, syncInfo.calendarTimezone);
- mContentResolver.update(
- ContentUris.withAppendedId(Calendars.CONTENT_URI, syncInfo.calendarId),
- map, null, null);
- }
- } finally {
- cursor.close();
- }
- }
-
- @Override
- protected void initTempProvider(SyncableContentProvider cp) {
- // TODO: don't use the real db's calendar id's. create new ones locally and translate
- // during CalendarProvider's merge.
-
- // populate temp provider with calendar ids, so joins work.
- ContentValues map = new ContentValues();
- final Account account = getAccount();
- Cursor c = getContext().getContentResolver().query(
- Calendar.Calendars.CONTENT_URI,
- CALENDARS_PROJECTION,
- SELECT_BY_ACCOUNT, new String[]{account.name, account.type},
- null /* sort order */);
- final int idIndex = c.getColumnIndexOrThrow(Calendars._ID);
- final int urlIndex = c.getColumnIndexOrThrow(Calendars.URL);
- final int timezoneIndex = c.getColumnIndexOrThrow(Calendars.TIMEZONE);
- final int ownerAccountIndex = c.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
- while (c.moveToNext()) {
- map.clear();
- map.put(Calendars._ID, c.getLong(idIndex));
- map.put(Calendars.URL, c.getString(urlIndex));
- map.put(Calendars.TIMEZONE, c.getString(timezoneIndex));
- map.put(Calendars.OWNER_ACCOUNT, c.getString(ownerAccountIndex));
- cp.insert(Calendar.Calendars.CONTENT_URI, map);
- }
- c.close();
- }
-
- public void onAccountsChanged(Account[] accountsArray) {
- // - Get a cursor (A) over all sync'd calendars over all accounts
- // - Get a cursor (B) over all subscribed feeds for calendar
- // - If an item is in A but not B then add a subscription
- // - If an item is in B but not A then remove the subscription
-
- ContentResolver cr = getContext().getContentResolver();
- Cursor cursorA = null;
- Cursor cursorB = null;
- try {
- cursorA = Calendar.Calendars.query(cr, null /* projection */,
- Calendar.Calendars.SYNC_EVENTS + "=1", CALENDAR_KEY_SORT_ORDER);
- int urlIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars.URL);
- int accountNameIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
- int accountTypeIndexA =
- cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT_TYPE);
- cursorB = SubscribedFeeds.Feeds.query(cr, FEEDS_KEY_COLUMNS,
- SubscribedFeeds.Feeds.AUTHORITY + "=?", new String[]{Calendar.AUTHORITY},
- FEEDS_KEY_SORT_ORDER);
- if (cursorB == null) {
- // This will happen if subscribed feeds are not installed. Get out since there
- // are no feeds to manipulate.
- return;
- }
- int urlIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds.FEED);
- int accountNameIndexB =
- cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
- int accountTypeIndexB =
- cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE);
- for (CursorJoiner.Result joinerResult :
- new CursorJoiner(cursorA, CALENDAR_KEY_COLUMNS, cursorB, FEEDS_KEY_COLUMNS)) {
- switch (joinerResult) {
- case LEFT:
- SubscribedFeeds.addFeed(
- cr,
- cursorA.getString(urlIndexA),
- new Account(cursorA.getString(accountNameIndexA),
- cursorA.getString(accountTypeIndexA)),
- Calendar.AUTHORITY,
- CalendarClient.SERVICE);
- break;
- case RIGHT:
- SubscribedFeeds.deleteFeed(
- cr,
- cursorB.getString(urlIndexB),
- new Account(cursorB.getString(accountNameIndexB),
- cursorB.getString(accountTypeIndexB)),
- Calendar.AUTHORITY);
- break;
- case BOTH:
- // do nothing, since the subscription already exists
- break;
- }
- }
- } finally {
- // check for null in case an exception occurred before the cursors got created
- if (cursorA != null) cursorA.close();
- if (cursorB != null) cursorB.close();
- }
- }
-
- /**
- * Should not get called. The feed url changes depending on which calendar is being sync'd
- * to/from the device, and thus is determined and passed around as a local variable, where
- * appropriate.
- */
- protected String getFeedUrl(Account account) {
- throw new UnsupportedOperationException("getFeedUrl() should not get called.");
- }
-
- protected Class getFeedEntryClass() {
- return EventEntry.class;
- }
-
- // XXX temporary debugging
- private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) {
- sb.append("[");
- for (String key : bundle.keySet()) {
- sb.append(key).append("=").append(bundle.get(key)).append(" ");
- }
- sb.append("]");
- }
-
-
- @Override
- protected void updateQueryParameters(QueryParams params, GDataSyncData.FeedData feedSyncData) {
- if (feedSyncData != null && feedSyncData.newWindowEnd > 0) {
- // Advancing the sliding window: set the parameters to the new part of the window
- params.setUpdatedMin(null);
- params.setParamValue("requirealldeleted", "false");
- Time startMinTime = new Time(Time.TIMEZONE_UTC);
- Time startMaxTime = new Time(Time.TIMEZONE_UTC);
- startMinTime.set(feedSyncData.windowEnd);
- startMaxTime.set(feedSyncData.newWindowEnd);
- String startMin = startMinTime.format("%Y-%m-%dT%H:%M:%S.000Z");
- String startMax = startMaxTime.format("%Y-%m-%dT%H:%M:%S.000Z");
- params.setParamValue("start-min", startMin);
- params.setParamValue("start-max", startMax);
- } else if (params.getUpdatedMin() == null) {
- // if this is the first sync, only bother syncing starting from
- // one month ago.
- // TODO: remove this restriction -- we may want all of
- // historical calendar events.
- Time lastMonth = new Time(Time.TIMEZONE_UTC);
- lastMonth.setToNow();
- --lastMonth.month;
- lastMonth.normalize(true /* ignore isDst */);
- // TODO: move start-min to CalendarClient?
- // or create CalendarQueryParams subclass (extra class)?
- String startMin = lastMonth.format("%Y-%m-%dT%H:%M:%S.000Z");
- params.setParamValue("start-min", startMin);
- // Note: start-max is not set for regular syncs. The sync needs to pick up events
- // outside the window in case an event inside the window got moved outside.
- // The event will be discarded later.
- }
-
- // HACK: specify that we want to expand recurrences in the past,
- // so the server does not expand any recurrences. we do this to
- // avoid a large number of gd:when elements that we do not need,
- // since we process gd:recurrence elements instead.
- params.setParamValue("recurrence-expansion-start", "1970-01-01");
- params.setParamValue("recurrence-expansion-end", "1970-01-01");
- // we want to get the events ordered by last modified, so we can
- // recover in case we cannot process the entire feed.
- params.setParamValue("orderby", "lastmodified");
- params.setParamValue("sortorder", "ascending");
- }
-
- @Override
- protected GDataServiceClient getGDataServiceClient() {
- return mCalendarClient;
- }
-
- protected void getStatsString(StringBuffer sb, SyncResult result) {
- super.getStatsString(sb, result);
- if (mRefresh > 0) {
- sb.append("F").append(mRefresh);
- }
- if (mServerDiffs > 0) {
- sb.append("s").append(mServerDiffs);
- }
- }
-
- private void fetchCalendarsFromServer() {
- if (mCalendarClient == null) {
- Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
- return;
- }
-
- Account account = null;
- String authToken = null;
-
-
- try {
- // TODO: allow caller to specify which account's feeds should be updated
- String[] features = new String[]{
- GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE};
- Account[] accounts = AccountManager.get(getContext()).getAccountsByTypeAndFeatures(
- GoogleLoginServiceConstants.ACCOUNT_TYPE, features, null, null).getResult();
- if (accounts.length == 0) {
- Log.w(TAG, "Unable to update calendars from server -- no users configured.");
- return;
- }
-
- account = accounts[0];
-
- Bundle bundle = AccountManager.get(getContext()).getAuthToken(
- account, mCalendarClient.getServiceName(),
- true /* notifyAuthFailure */, null /* callback */, null /* handler */)
- .getResult();
- authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
- if (authToken == null) {
- Log.w(TAG, "Unable to update calendars from server -- could not "
- + "authenticate user " + account);
- return;
- }
- } catch (IOException e) {
- Log.w(TAG, "Unable to update calendars from server -- could not "
- + "authenticate user " + account, e);
- return;
- } catch (AuthenticatorException e) {
- Log.w(TAG, "Unable to update calendars from server -- could not "
- + "authenticate user " + account, e);
- return;
- } catch (OperationCanceledException e) {
- Log.w(TAG, "Unable to update calendars from server -- could not "
- + "authenticate user " + account, e);
- return;
- }
-
- // get the current set of calendars. we'll need to pay attention to
- // which calendars we get back from the server, so we can delete
- // calendars that have been deleted from the server.
- Set<Long> existingCalendarIds = new HashSet<Long>();
-
- getCurrentCalendars(existingCalendarIds);
-
- // get and process the calendars meta feed
- GDataParser parser = null;
- try {
- String feedUrl = mCalendarClient.getUserCalendarsUrl(account.name);
- feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
- parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
- // process the calendars
- processCalendars(account, parser, existingCalendarIds);
- } catch (ParseException pe) {
- Log.w(TAG, "Unable to process calendars from server -- could not "
- + "parse calendar feed.", pe);
- return;
- } catch (IOException ioe) {
- Log.w(TAG, "Unable to process calendars from server -- encountered "
- + "i/o error", ioe);
- return;
- } catch (HttpException e) {
- switch (e.getStatusCode()) {
- case HttpException.SC_UNAUTHORIZED:
- Log.w(TAG, "Unable to process calendars from server -- could not "
- + "authenticate user.", e);
- return;
- case HttpException.SC_GONE:
- Log.w(TAG, "Unable to process calendars from server -- encountered "
- + "an AllDeletedUnavailableException, this should never happen", e);
- return;
- default:
- Log.w(TAG, "Unable to process calendars from server -- error", e);
- return;
- }
- } finally {
- if (parser != null) {
- parser.close();
- }
- }
-
- // delete calendars that are no longer sent from the server.
- final Uri calendarContentUri = Calendars.CONTENT_URI;
- final ContentResolver cr = getContext().getContentResolver();
- for (long calId : existingCalendarIds) {
- // NOTE: triggers delete all events, instances for this calendar.
- cr.delete(ContentUris.withAppendedId(calendarContentUri, calId),
- null /* where */, null /* selectionArgs */);
- }
- }
-
- private void getCurrentCalendars(Set<Long> calendarIds) {
- final ContentResolver cr = getContext().getContentResolver();
- Cursor cursor = cr.query(Calendars.CONTENT_URI,
- new String[] { Calendars._ID },
- null /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (cursor != null) {
- try {
- while (cursor.moveToNext()) {
- calendarIds.add(cursor.getLong(0));
- }
- } finally {
- cursor.close();
- }
- }
- }
-
- private void processCalendars(Account account,
- GDataParser parser,
- Set<Long> existingCalendarIds)
- throws ParseException, IOException {
- final ContentResolver cr = getContext().getContentResolver();
- CalendarsFeed feed = (CalendarsFeed) parser.init();
- Entry entry = null;
- final Uri calendarContentUri = Calendars.CONTENT_URI;
- ArrayList<ContentValues> inserts = new ArrayList<ContentValues>();
- while (parser.hasMoreData()) {
- entry = parser.readNextEntry(entry);
- if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
- CalendarEntry calendarEntry = (CalendarEntry) entry;
- ContentValues map = new ContentValues();
- String feedUrl = calendarEntryToContentValues(account, feed, calendarEntry, map);
- if (TextUtils.isEmpty(feedUrl)) {
- continue;
- }
- long calId = -1;
-
- Cursor c = cr.query(calendarContentUri,
- new String[] { Calendars._ID },
- Calendars.URL + "='"
- + feedUrl + '\'' /* selection */,
- null /* selectionArgs */,
- null /* sort */);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- calId = c.getLong(0);
- existingCalendarIds.remove(calId);
- }
- } finally {
- c.close();
- }
- }
-
- if (calId != -1) {
- if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
- // don't override the existing "selected" or "hidden" settings.
- map.remove(Calendars.SELECTED);
- map.remove(Calendars.HIDDEN);
- cr.update(ContentUris.withAppendedId(calendarContentUri, calId), map,
- null /* where */, null /* selectionArgs */);
- } else {
- // Select this calendar for syncing and display if it is
- // selected and not hidden.
- int syncAndDisplay = 0;
- if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
- syncAndDisplay = 1;
- }
- map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
- map.put(Calendars.SELECTED, syncAndDisplay);
- map.put(Calendars.HIDDEN, 0);
- map.put(Calendars._SYNC_ACCOUNT, account.name);
- map.put(Calendars._SYNC_ACCOUNT_TYPE, account.type);
- if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
- inserts.add(map);
- }
- }
- if (!inserts.isEmpty()) {
- if (Config.LOGV) Log.v(TAG, "Bulk updating calendar list.");
- cr.bulkInsert(calendarContentUri, inserts.toArray(new ContentValues[inserts.size()]));
- }
- }
-
- /**
- * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
- * Calendars table.
- */
- private String calendarEntryToContentValues(Account account, CalendarsFeed feed,
- CalendarEntry entry,
- ContentValues map) {
- map.clear();
-
- String url = entry.getAlternateLink();
-
- if (TextUtils.isEmpty(url)) {
- // yuck. the alternate link was not available. we should
- // reconstruct from the id.
- url = entry.getId();
- if (!TextUtils.isEmpty(url)) {
- url = convertCalendarIdToFeedUrl(url);
- } else {
- if (Config.LOGV) {
- Log.v(TAG, "Cannot generate url for calendar feed.");
- }
- return null;
- }
- }
-
- url = rewriteUrlforAccount(account, url);
-
- map.put(Calendars.URL, url);
- map.put(Calendars.OWNER_ACCOUNT, calendarEmailAddressFromFeedUrl(url));
- map.put(Calendars.NAME, entry.getTitle());
-
- // TODO:
- map.put(Calendars.DISPLAY_NAME, entry.getTitle());
-
- map.put(Calendars.TIMEZONE, entry.getTimezone());
-
- String colorStr = entry.getColor();
- if (!TextUtils.isEmpty(colorStr)) {
- int color = Color.parseColor(colorStr);
- // Ensure the alpha is set to max
- color |= 0xff000000;
- map.put(Calendars.COLOR, color);
- }
-
- map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
-
- map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
-
- int accesslevel;
- switch (entry.getAccessLevel()) {
- case CalendarEntry.ACCESS_NONE:
- accesslevel = Calendars.NO_ACCESS;
- break;
- case CalendarEntry.ACCESS_READ:
- accesslevel = Calendars.READ_ACCESS;
- break;
- case CalendarEntry.ACCESS_FREEBUSY:
- accesslevel = Calendars.FREEBUSY_ACCESS;
- break;
- case CalendarEntry.ACCESS_EDITOR:
- accesslevel = Calendars.EDITOR_ACCESS;
- break;
- case CalendarEntry.ACCESS_OWNER:
- accesslevel = Calendars.OWNER_ACCESS;
- break;
- case CalendarEntry.ACCESS_ROOT:
- accesslevel = Calendars.ROOT_ACCESS;
- break;
- default:
- accesslevel = Calendars.NO_ACCESS;
- }
- map.put(Calendars.ACCESS_LEVEL, accesslevel);
- // TODO: use the update time, when calendar actually supports this.
- // right now, calendar modifies the update time frequently.
- map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
-
- return url;
- }
-
- // TODO: unit test.
- protected static final String convertCalendarIdToFeedUrl(String url) {
- // id: http://www.google.com/calendar/feeds/<username>/<cal id>
- // desired feed:
- // http://www.google.com/calendar/feeds/<cal id>/<projection>
- int start = url.indexOf(FEEDS_SUBSTRING);
- if (start != -1) {
- // strip out the */ in /feeds/*/
- start += FEEDS_SUBSTRING.length();
- int end = url.indexOf('/', start);
- if (end != -1) {
- url = url.replace(url.substring(start, end + 1), "");
- }
- url = url + PRIVATE_FULL;
- }
- return url;
- }
-
- /**
- * Extracts the calendar email from a calendar feed url.
- * @param feed the calendar feed url
- * @return the calendar email that is in the feed url or null if it can't
- * find the email address.
- */
- public static String calendarEmailAddressFromFeedUrl(String feed) {
- // Example feed url:
- // https://www.google.com/calendar/feeds/foo%40gmail.com/private/full-noattendees
- String[] pathComponents = feed.split("/");
- if (pathComponents.length > 5 && "feeds".equals(pathComponents[4])) {
- try {
- return URLDecoder.decode(pathComponents[5], "UTF-8");
- } catch (UnsupportedEncodingException e) {
- Log.e(TAG, "unable to url decode the email address in calendar " + feed);
- return null;
- }
- }
-
- Log.e(TAG, "unable to find the email address in calendar " + feed);
- return null;
- }
-}
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapterService.java b/src/com/android/providers/calendar/CalendarSyncAdapterService.java
deleted file mode 100644
index 7634f2c..0000000
--- a/src/com/android/providers/calendar/CalendarSyncAdapterService.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.android.providers.calendar;
-
-import android.app.Service;
-import android.os.IBinder;
-import android.content.Intent;
-import android.content.ContentProviderClient;
-import android.content.ContentProvider;
-import android.content.SyncableContentProvider;
-import android.provider.Calendar;
-
-public class CalendarSyncAdapterService extends Service {
- private ContentProviderClient mContentProviderClient = null;
-
- public void onCreate() {
- mContentProviderClient =
- getContentResolver().acquireContentProviderClient(Calendar.CONTENT_URI);
- }
-
- public void onDestroy() {
- mContentProviderClient.release();
- }
-
- public IBinder onBind(Intent intent) {
- ContentProvider contentProvider = mContentProviderClient.getLocalContentProvider();
- if (contentProvider == null) throw new IllegalStateException();
- SyncableContentProvider syncableContentProvider = (SyncableContentProvider)contentProvider;
- return syncableContentProvider.getTempProviderSyncAdapter().getISyncAdapter().asBinder();
- }
-}
diff --git a/src/com/android/providers/calendar/MetaData.java b/src/com/android/providers/calendar/MetaData.java
index ed7f9d9..fc15e45 100644
--- a/src/com/android/providers/calendar/MetaData.java
+++ b/src/com/android/providers/calendar/MetaData.java
@@ -41,15 +41,13 @@
public String timezone; // local timezone used for Instance expansion
public long minInstance; // UTC millis
public long maxInstance; // UTC millis
- public int minBusyBit; // Julian start day
- public int maxBusyBit; // Julian end day
}
-
+
/**
* The cached copy of the meta-data fields from the database.
*/
private Fields mFields = new Fields();
-
+
private final SQLiteOpenHelper mOpenHelper;
private boolean mInitialized;
@@ -60,24 +58,20 @@
private static final String[] sCalendarMetaDataProjection = {
CalendarMetaData.LOCAL_TIMEZONE,
CalendarMetaData.MIN_INSTANCE,
- CalendarMetaData.MAX_INSTANCE,
- CalendarMetaData.MIN_BUSYBITS,
- CalendarMetaData.MAX_BUSYBITS };
-
+ CalendarMetaData.MAX_INSTANCE};
+
private static final int METADATA_INDEX_LOCAL_TIMEZONE = 0;
private static final int METADATA_INDEX_MIN_INSTANCE = 1;
private static final int METADATA_INDEX_MAX_INSTANCE = 2;
- private static final int METADATA_INDEX_MIN_BUSYBIT = 3;
- private static final int METADATA_INDEX_MAX_BUSYBIT = 4;
-
+
public MetaData(SQLiteOpenHelper openHelper) {
mOpenHelper = openHelper;
}
-
+
/**
* Returns a copy of all the MetaData fields. This method grabs a
* database lock to read all the fields atomically.
- *
+ *
* @return a copy of all the MetaData fields.
*/
public Fields getFields() {
@@ -93,8 +87,6 @@
fields.timezone = mFields.timezone;
fields.minInstance = mFields.minInstance;
fields.maxInstance = mFields.maxInstance;
- fields.minBusyBit = mFields.minBusyBit;
- fields.maxBusyBit = mFields.maxBusyBit;
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -104,12 +96,12 @@
/**
* This method must be called only while holding a database lock.
- *
+ *
* <p>
* Returns a copy of all the MetaData fields. This method assumes
* the database lock has already been acquired.
* </p>
- *
+ *
* @return a copy of all the MetaData fields.
*/
public Fields getFieldsLocked() {
@@ -124,11 +116,9 @@
fields.timezone = mFields.timezone;
fields.minInstance = mFields.minInstance;
fields.maxInstance = mFields.maxInstance;
- fields.minBusyBit = mFields.minBusyBit;
- fields.maxBusyBit = mFields.maxBusyBit;
return fields;
}
-
+
/**
* Reads the meta-data for the CalendarProvider from the database and
* updates the member variables. This method executes while the database
@@ -138,7 +128,6 @@
private void readLocked(SQLiteDatabase db) {
String timezone = null;
long minInstance = 0, maxInstance = 0;
- int minBusyBit = 0, maxBusyBit = 0;
// Read the database directly. We only do this once to initialize
// the members of this class.
@@ -149,8 +138,6 @@
timezone = cursor.getString(METADATA_INDEX_LOCAL_TIMEZONE);
minInstance = cursor.getLong(METADATA_INDEX_MIN_INSTANCE);
maxInstance = cursor.getLong(METADATA_INDEX_MAX_INSTANCE);
- minBusyBit = cursor.getInt(METADATA_INDEX_MIN_BUSYBIT);
- maxBusyBit = cursor.getInt(METADATA_INDEX_MAX_BUSYBIT);
}
} finally {
if (cursor != null) {
@@ -162,9 +149,7 @@
mFields.timezone = timezone;
mFields.minInstance = minInstance;
mFields.maxInstance = maxInstance;
- mFields.minBusyBit = minBusyBit;
- mFields.maxBusyBit = maxBusyBit;
-
+
// Mark the fields as initialized
mInitialized = true;
}
@@ -173,7 +158,7 @@
* Writes the meta-data for the CalendarProvider. The values to write are
* passed in as parameters. All of the values are updated atomically,
* including the cached copy of the meta-data.
- *
+ *
* @param timezone the local timezone used for Instance expansion
* @param begin the start of the Instance expansion in UTC milliseconds
* @param end the end of the Instance expansion in UTC milliseconds
@@ -184,7 +169,7 @@
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
db.beginTransaction();
try {
- writeLocked(timezone, begin, end, startDay, endDay);
+ writeLocked(timezone, begin, end);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -193,27 +178,23 @@
/**
* This method must be called only while holding a database lock.
- *
+ *
* <p>
* Writes the meta-data for the CalendarProvider. The values to write are
* passed in as parameters. All of the values are updated atomically,
* including the cached copy of the meta-data.
* </p>
- *
+ *
* @param timezone the local timezone used for Instance expansion
* @param begin the start of the Instance expansion in UTC milliseconds
* @param end the end of the Instance expansion in UTC milliseconds
- * @param startDay the start of the BusyBit expansion (the start Julian day)
- * @param endDay the end of the BusyBit expansion (the end Julian day)
*/
- public void writeLocked(String timezone, long begin, long end, int startDay, int endDay) {
+ public void writeLocked(String timezone, long begin, long end) {
ContentValues values = new ContentValues();
values.put("_id", 1);
values.put(CalendarMetaData.LOCAL_TIMEZONE, timezone);
values.put(CalendarMetaData.MIN_INSTANCE, begin);
values.put(CalendarMetaData.MAX_INSTANCE, end);
- values.put(CalendarMetaData.MIN_BUSYBITS, startDay);
- values.put(CalendarMetaData.MAX_BUSYBITS, endDay);
// Atomically update the database and the cached members.
try {
@@ -223,7 +204,6 @@
// Failed: zero the in-memory fields to force recomputation.
mFields.timezone = null;
mFields.minInstance = mFields.maxInstance = 0;
- mFields.minBusyBit = mFields.maxBusyBit = 0;
throw e;
}
@@ -231,15 +211,13 @@
mFields.timezone = timezone;
mFields.minInstance = begin;
mFields.maxInstance = end;
- mFields.minBusyBit = startDay;
- mFields.maxBusyBit = endDay;
}
/**
* Clears the time range for the Instances table. The rows in the
* Instances table will be deleted (and regenerated) the next time
* that the Instances table is queried.
- *
+ *
* Also clears the time range for the BusyBits table because that depends
* on the Instances table.
*/
@@ -252,28 +230,7 @@
if (!mInitialized) {
readLocked(db);
}
- writeLocked(mFields.timezone, 0 /* begin */, 0 /* end */,
- 0 /* startDay */, 0 /* endDay */);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- /**
- * Clears the time range for the BusyBits table.
- */
- public void clearBusyBitRange() {
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- db.beginTransaction();
- try {
- // If the fields have not been initialized from the database,
- // then read the database.
- if (!mInitialized) {
- readLocked(db);
- }
- writeLocked(mFields.timezone, mFields.minInstance, mFields.maxInstance,
- 0 /* startDay */, 0 /* endDay */);
+ writeLocked(mFields.timezone, 0 /* begin */, 0 /* end */);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
diff --git a/src/com/android/providers/calendar/SQLiteContentProvider.java b/src/com/android/providers/calendar/SQLiteContentProvider.java
new file mode 100644
index 0000000..6dedddb
--- /dev/null
+++ b/src/com/android/providers/calendar/SQLiteContentProvider.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2009 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.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider
+ implements SQLiteTransactionListener {
+
+ private static final String TAG = "SQLiteContentProvider";
+
+ private SQLiteOpenHelper mOpenHelper;
+ private volatile boolean mNotifyChange;
+ protected SQLiteDatabase mDb;
+
+ private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+ private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+
+ @Override
+ public boolean onCreate() {
+ Context context = getContext();
+ mOpenHelper = getDatabaseHelper(context);
+ return true;
+ }
+
+ protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+
+ /**
+ * The equivalent of the {@link #insert} method, but invoked within a transaction.
+ */
+ protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
+
+ /**
+ * The equivalent of the {@link #update} method, but invoked within a transaction.
+ */
+ protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs);
+
+ /**
+ * The equivalent of the {@link #delete} method, but invoked within a transaction.
+ */
+ protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+
+ protected abstract void notifyChange();
+
+ protected SQLiteOpenHelper getDatabaseHelper() {
+ return mOpenHelper;
+ }
+
+ private boolean applyingBatch() {
+ return mApplyingBatch.get() != null && mApplyingBatch.get();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ Uri result = null;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ result = insertInTransaction(uri, values);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ result = insertInTransaction(uri, values);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ int numValues = values.length;
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ for (int i = 0; i < numValues; i++) {
+ Uri result = insertInTransaction(uri, values[i]);
+ if (result != null) {
+ mNotifyChange = true;
+ }
+ mDb.yieldIfContendedSafely();
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ return numValues;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ count = updateInTransaction(uri, values, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ count = updateInTransaction(uri, values, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ }
+
+ return count;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ count = deleteInTransaction(uri, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDb.endTransaction();
+ }
+
+ onEndTransaction();
+ } else {
+ count = deleteInTransaction(uri, selection, selectionArgs);
+ if (count > 0) {
+ mNotifyChange = true;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ mDb = mOpenHelper.getWritableDatabase();
+ mDb.beginTransactionWithListener(this);
+ try {
+ mApplyingBatch.set(true);
+ final int numOperations = operations.size();
+ final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+ for (int i = 0; i < numOperations; i++) {
+ final ContentProviderOperation operation = operations.get(i);
+ if (i > 0 && operation.isYieldAllowed()) {
+ mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
+ }
+ results[i] = operation.apply(this, results, i);
+ }
+ mDb.setTransactionSuccessful();
+ return results;
+ } finally {
+ mApplyingBatch.set(false);
+ mDb.endTransaction();
+ onEndTransaction();
+ }
+ }
+
+ public void onBegin() {
+ onBeginTransaction();
+ }
+
+ public void onCommit() {
+ beforeTransactionCommit();
+ }
+
+ public void onRollback() {
+ // not used
+ }
+
+ protected void onBeginTransaction() {
+ }
+
+ protected void beforeTransactionCommit() {
+ }
+
+ protected void onEndTransaction() {
+ if (mNotifyChange) {
+ mNotifyChange = false;
+ notifyChange();
+ }
+ }
+}
diff --git a/src/com/android/providers/calendar/VCal.java b/src/com/android/providers/calendar/VCal.java
deleted file mode 100644
index d292755..0000000
--- a/src/com/android/providers/calendar/VCal.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/* //device/apps/Calendar/MonthView.java
-**
-** Copyright 2006, 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.pim.DateException;
-import android.text.format.DateUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-import java.util.regex.Pattern;
-
-public class VCal
-{
- public static final Pattern LINE = Pattern.compile(
- "([^:;]+)([^:]*):(.*)");
-
- public ArrayList<Property> properties = new ArrayList<Property>();
- public String dtstart;
- public String tzid;
- public String duration;
- public String rrule;
- public boolean allDay;
-
- public void dump()
- {
- System.out.println("-----------------------");
- dump(properties, "");
- System.out.println("dtstart='" + this.dtstart + "'");
- System.out.println("tzid='" + this.tzid + "'");
- System.out.println("duration='" + this.duration + "'");
- System.out.println("rrule='" + this.rrule + "'");
- System.out.println("-----------------------");
- }
-
- public static void dump(ArrayList<Property> props, String prefix)
- {
- int count = props.size();
- for (int i=0; i<count; i++) {
- Property prop = props.get(i);
- System.out.println(prefix + prop.name);
- if (prop instanceof Begin) {
- Begin b = (Begin)prop;
- dump(b.properties, prefix + " ");
- }
- }
- }
-
- public static class Parameter
- {
- public String name;
- public String value;
- }
-
- public static class Property
- {
- public String name;
- public Parameter[] parameters;
- public String value;
- public String[] values;
- }
-
- public static class Begin extends Property
- {
- public Begin parent;
- public ArrayList<Property> properties = new ArrayList<Property>();
- }
-
-
- public static Property make(String name)
- {
- Property p;
- if (name.equals("BEGIN")) {
- p = new Begin();
- }
- else {
- p = new Property();
- }
- p.name = name;
- return p;
- }
-
- public static VCal parse(String str)
- {
- VCal vc = new VCal();
-
- int i, j, start;
- int N, M;
-
- // first we deal with line folding, by replacing all "\r\n " strings
- // with nothing
- str = str.replaceAll("\r\n ", "");
-
- // it's supposed to be \r\n, but not everyone does that
- str = str.replaceAll("\r\n", "\n");
- str = str.replaceAll("\r", "\n");
-
- ArrayList<Parameter> params = new ArrayList<Parameter>();
- ArrayList<Property> props = vc.properties;
-
- // then we split into lines
- String[] lines = str.split("\n");
-
- Begin begin = null;
- //System.out.println("lines.length=" + lines);
- N = lines.length;
- for (j=0; j<N; j++) {
- //System.out.println("===[" + lines[j] + "]===");
- String line = lines[j];
- int len = line.length();
- if (len > 0) {
- i = 0;
- char c;
- do {
- c = line.charAt(i);
- i++;
- } while (c != ';' && c != ':');
-
- String n = line.substring(0, i-1);
- Property prop = make(n);
- props.add(prop);
- if (n.equals("BEGIN")) {
- Begin b = (Begin)prop;
- b.parent = begin;
- begin = b;
- props = begin.properties;
- }
- else if (n.equals("END")) {
- begin = begin.parent;
- if (begin != null) {
- props = begin.properties;
- } else {
- props = vc.properties;
- }
- }
-
- //System.out.println("name=[" + prop.name + "]");
- params.clear();
- while (c == ';') {
- Parameter param = new Parameter();
- start = i;
- i++;
- // param name
- do {
- c = line.charAt(i);
- i++;
- } while (c != '=');
- param.name = line.substring(start, i-1);
- //System.out.println(" param.name=[" + param.name + "]");
- start = i;
- if (line.charAt(start) == '"') {
- i++;
- start++;
- do {
- c = line.charAt(i);
- i++;
- } while (c != '"');
- param.value = line.substring(start, i-1);
- c = line.charAt(i);
- i++;
- //System.out.println(" param.valueA=[" + param.value
- // + "]");
- } else {
- do {
- c = line.charAt(i);
- i++;
- } while (c != ';' && c != ':');
- param.value = line.substring(start, i-1);
- //System.out.println(" param.valueB=["
- // + param.value + "]");
- }
- params.add(param);
- }
- Object[] array = params.toArray();
- prop.parameters = new Parameter[array.length];
- System.arraycopy(array, 0, prop.parameters, 0, array.length);
- if (c != ':') {
- throw new RuntimeException("error finding ':' c=" + c);
- }
- prop.value = line.substring(i);
- prop.values = line.split(",");
- }
- }
-
- N = vc.properties.size();
- Calendar calStart = null;
- for (i=0; i<N; i++) {
- Property prop = vc.properties.get(i);
- String n = prop.name;
- if (n.equals("DTSTART")) {
- try {
- calStart = parseDateTime(prop, vc);
- vc.dtstart = prop.value;
- } catch (DateException de) {
- Log.w("CalendarProvider", "Unable to parse DTSTART=" + n, de);
- return null;
- }
- } else if (n.equals("DTEND")) {
- // TODO: store the dtend, compute when expanding instances?
- // will we ever need to deal with seeing the DTEND before the
- // DTSTART?
- try {
- if (calStart == null) {
- vc.duration = "+P0S";
- } else {
- Calendar calEnd =
- parseDateTime(prop, vc);
- long durationMillis =
- calEnd.getTimeInMillis() -
- calStart.getTimeInMillis();
- long durationSeconds = (durationMillis / 1000);
- vc.duration = "+P" + durationSeconds + "S";
- }
- } catch (DateException de) {
- Log.w("CalendarProvider", "Unable to parse DTEND=" + n, de);
- return null;
- }
- } else if (n.equals("DURATION")) {
- vc.duration = prop.value;
- } else if (n.equals("RRULE")) {
- vc.rrule = prop.value;
- }
- }
- return vc;
- }
-
- private static Calendar parseDateTime(Property prop, VCal vc) throws DateException {
- int M;
- int j;
- String dt = prop.value;
- M = prop.parameters.length;
- for (j=0; j<M; j++) {
- Parameter param = prop.parameters[j];
- if (param.name.equals("TZID")) {
- vc.tzid = param.value;
- }
- }
-
- TimeZone tz = TimeZone.getTimeZone(vc.tzid);
- if (tz == null) {
- tz = TimeZone.getTimeZone("UTC");
- }
- GregorianCalendar somewhere = new GregorianCalendar(tz);
- DateUtils.parseDateTime(dt, somewhere);
- if (dt.length() == 8) {
- // this seems to work.
- vc.allDay = true;
- }
- return somewhere;
- /*GregorianCalendar zulu = new GregorianCalendar(
- TimeZone.getTimeZone("GMT"));
- zulu.setTimeInMillis(somewhere.getTimeInMillis());
- return zulu;*/
- // System.out.println("DTSTART=" + dtstart
- // + " somewhere=" + somewhere
- // + " vc.dtstart=" + vc.dtstart);
- }
-}
diff --git a/tests/Android.mk b/tests/Android.mk
index c6b3875..6fa46fb 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -9,7 +9,7 @@
LOCAL_PACKAGE_NAME := CalendarProviderTests
-LOCAL_JAVA_LIBRARIES := ext android.test.runner googlelogin-client
+LOCAL_JAVA_LIBRARIES := ext android.test.runner gsf-client
LOCAL_INSTRUMENTATION_FOR := CalendarProvider
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
new file mode 100644
index 0000000..7793b0f
--- /dev/null
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2ForTesting.java
@@ -0,0 +1,11 @@
+package com.android.providers.calendar;
+
+public class CalendarProvider2ForTesting extends CalendarProvider2 {
+ /**
+ * For testing, don't want to start the TimezoneCheckerThread, as it results
+ * in race conditions. Thus updateTimezoneDependentFields is stubbed out.
+ */
+ @Override
+ protected void updateTimezoneDependentFields() {
+ }
+}
diff --git a/tests/src/com/android/providers/calendar/CalendarProviderTest.java b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
similarity index 68%
rename from tests/src/com/android/providers/calendar/CalendarProviderTest.java
rename to tests/src/com/android/providers/calendar/CalendarProvider2Test.java
index 9e9e6e5..39f6828 100644
--- a/tests/src/com/android/providers/calendar/CalendarProviderTest.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
@@ -16,7 +16,9 @@
package com.android.providers.calendar;
-import com.android.internal.database.ArrayListCursor;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.TimeUtils;
+import com.android.common.ArrayListCursor;
import android.content.*;
import android.database.Cursor;
@@ -25,9 +27,9 @@
import android.text.format.DateUtils;
import android.text.format.Time;
import android.provider.Calendar;
-import android.provider.Calendar.BusyBits;
import android.provider.Calendar.Calendars;
import android.provider.Calendar.Events;
+import android.provider.Calendar.EventsEntity;
import android.provider.Calendar.Instances;
import android.test.ProviderTestCase2;
import android.test.mock.MockContentResolver;
@@ -37,27 +39,34 @@
import java.util.ArrayList;
-
/**
* Runs various tests on an isolated Calendar provider with its own database.
*/
-public class CalendarProviderTest extends ProviderTestCase2<CalendarProvider> {
+@LargeTest
+public class CalendarProvider2Test extends ProviderTestCase2<CalendarProvider2ForTesting> {
static final String TAG = "calendar";
- static final String DEFAULT_TIMEZONE = "America/Los_Angeles";
private SQLiteDatabase mDb;
private MetaData mMetaData;
private Context mContext;
private MockContentResolver mResolver;
- private Uri mEventsUri = Uri.parse("content://calendar/events");
+ private Uri mEventsUri = Events.CONTENT_URI;
private int mCalendarId;
protected boolean mWipe = 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";
+
/**
* KeyValue is a simple class that stores a pair of strings representing
* a (key, value) pair. This is used for updating events.
@@ -65,7 +74,7 @@
private class KeyValue {
String key;
String value;
-
+
public KeyValue(String key, String value) {
this.key = key;
this.value = value;
@@ -80,7 +89,7 @@
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
@@ -89,36 +98,36 @@
*/
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;
int expected;
-
+
public Delete(String eventName, int expected) {
this.eventName = eventName;
this.expected = expected;
}
-
+
public void execute() {
Log.i(TAG, "delete " + eventName);
int rows = deleteMatchingEvents(eventName);
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
@@ -130,7 +139,7 @@
private class Update implements Command {
String eventName;
KeyValue[] pairs;
-
+
public Update(String eventName, KeyValue[] pairs) {
this.eventName = eventName;
this.pairs = pairs;
@@ -155,48 +164,48 @@
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);
@@ -204,14 +213,14 @@
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.
@@ -220,7 +229,7 @@
int expected;
long begin;
long end;
-
+
public QueryNumInstances(String startDate, String endDate, int expected) {
Time time = new Time(DEFAULT_TIMEZONE);
time.parse3339(startDate);
@@ -229,14 +238,14 @@
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
@@ -248,18 +257,18 @@
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];
@@ -288,7 +297,7 @@
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;
@@ -328,18 +337,18 @@
/**
* When this command runs it verifies that the given instance exists in
- * the given date range.
+ * 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
@@ -350,20 +359,20 @@
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;
@@ -386,7 +395,7 @@
cursor.close();
}
}
-
+
/**
* This class stores all the useful information about an event.
*/
@@ -408,13 +417,13 @@
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;
@@ -433,13 +442,13 @@
mRrule = null;
mAllDay = allDay;
}
-
+
// 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) {
@@ -476,7 +485,7 @@
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) {
@@ -494,13 +503,13 @@
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);
@@ -512,27 +521,7 @@
mExpectedOccurrences = expected;
}
}
-
- private class BusyBitInfo {
- EventInfo[] mEvents;
- int mStartDay;
- int mNumDays;
- int[] mBusyBits;
- int[] mAllDayCounts;
-
- public BusyBitInfo(EventInfo[] events, String startDate, int numDays,
- int[] busybits, int[] allDayCounts) {
- mEvents = events;
- Time time = new Time(DEFAULT_TIMEZONE);
- time.parse3339(startDate);
- long millis = time.toMillis(true /* ignore isDst */);
- mStartDay = Time.getJulianDay(millis, time.gmtoff);
- mNumDays = numDays;
- mBusyBits = busybits;
- mAllDayCounts = allDayCounts;
- }
- }
-
+
/**
* This is the main table of events. The events in this table are
* referred to by name in other places.
@@ -617,10 +606,10 @@
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),
@@ -632,12 +621,12 @@
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),
@@ -661,117 +650,11 @@
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 tables of events is used to test the BusyBit database table.
- */
- private EventInfo[] mBusyBitEvents = {
- new EventInfo("1: 12am - 1am", "2008-05-01T00:00:00", "2008-05-01T01:00:00", false),
- new EventInfo("2: 1:30am - 2am", "2008-05-02T01:30:00", "2008-05-02T02:00:00", false),
- new EventInfo("3: 3am - 5am", "2008-05-03T03:00:00", "2008-05-03T05:00:00", false),
- new EventInfo("4: 12am - 5am", "2008-05-04T00:00:00", "2008-05-04T05:00:00", false),
- new EventInfo("5: 1am - 2am", "2008-05-05T01:00:00", "2008-05-05T02:00:00", false),
- new EventInfo("5: 8am - 9am", "2008-05-05T08:00:00", "2008-05-05T09:00:00", false),
- new EventInfo("6: 1am - 10am", "2008-05-06T01:00:00", "2008-05-06T10:00:00", false),
- new EventInfo("6: 8am - 9am", "2008-05-06T08:00:00", "2008-05-06T09:00:00", false),
- new EventInfo("7: 1am - 5am", "2008-05-07T01:00:00", "2008-05-07T05:00:00", false),
- new EventInfo("7: 12am - 2am", "2008-05-07T00:00:00", "2008-05-07T02:00:00", false),
- new EventInfo("7: 8am - 9am", "2008-05-07T08:00:00", "2008-05-07T09:00:00", false),
- new EventInfo("7: 1pm - 2pm", "2008-05-07T13:00:00", "2008-05-07T14:00:00", false),
- new EventInfo("7: 3:30pm - 4:30pm", "2008-05-07T15:30:00", "2008-05-07T16:30:00", false),
- new EventInfo("7: 7pm - 8pm", "2008-05-07T19:00:00", "2008-05-07T20:00:00", false),
- new EventInfo("7: 6:30pm - 7:30pm", "2008-05-07T18:30:00", "2008-05-07T19:30:00", false),
- new EventInfo("7: 11pm - midnight", "2008-05-07T23:00:00", "2008-05-08T00:00:00", false),
- new EventInfo("8: 1am - 2am", "2008-05-08T01:00:00", "2008-05-08T02:00:00", false),
- new EventInfo("8: 3am - 4am", "2008-05-08T03:00:00", "2008-05-08T04:00:00", false),
- new EventInfo("8: 5am - 6am", "2008-05-08T05:00:00", "2008-05-08T06:00:00", false),
- new EventInfo("8: 7am - 8am", "2008-05-08T07:00:00", "2008-05-08T08:00:00", false),
- new EventInfo("8: 9am - 10am", "2008-05-08T09:00:00", "2008-05-08T10:00:00", false),
- new EventInfo("8: 11am - 12pm", "2008-05-08T11:00:00", "2008-05-08T12:00:00", false),
- new EventInfo("8: 1pm - 2pm", "2008-05-08T13:00:00", "2008-05-08T14:00:00", false),
- new EventInfo("8: 3pm - 4pm", "2008-05-08T15:00:00", "2008-05-08T16:00:00", false),
- new EventInfo("8: 5pm - 6pm", "2008-05-08T17:00:00", "2008-05-08T18:00:00", false),
- new EventInfo("8: 7pm - 8pm", "2008-05-08T19:00:00", "2008-05-08T20:00:00", false),
- new EventInfo("8: 9pm - 10pm", "2008-05-08T21:00:00", "2008-05-08T22:00:00", false),
- new EventInfo("8: 11pm - midnight", "2008-05-08T23:00:00", "2008-05-09T00:00:00", false),
- new EventInfo("10: 12am - midnight", "2008-05-10T00:00:00", "2008-05-11T00:00:00", false),
- new EventInfo("12: 1 day", "2008-05-12T00:00:00", "2008-05-13T00:00:00", true),
- new EventInfo("14: 1 day", "2008-05-14T00:00:00", "2008-05-15T00:00:00", true),
- new EventInfo("14: 2 days", "2008-05-14T00:00:00", "2008-05-16T00:00:00", true),
- new EventInfo("14: 3 days", "2008-05-14T00:00:00", "2008-05-17T00:00:00", true),
- new EventInfo("15: 1am - 2am", "2008-05-15T01:00:00", "2008-05-15T02:00:00", false),
- new EventInfo("16: 10am - 11am", "2008-05-16T10:00:00", "2008-05-16T11:00:00", false),
- new EventInfo("16: 11pm - midnight", "2008-05-16T23:00:00", "2008-05-17T00:00:00", false),
- };
- private EventInfo[] mBusyBitRepeatingEvents = {
- 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("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 4:30am to 5:30am",
- "2008-05-06T04:30:00", "2008-05-06T05:30:00",
- "FREQ=WEEKLY;INTERVAL=2;BYDAY=TU;WKST=MO", false),
- new EventInfo("weekly2", "weekly from 5/5/2008 on Mon 1 day",
- "2008-05-05T00:00:00", "2008-05-06T00:00:00",
- "FREQ=WEEKLY;BYDAY=MO;WKST=MO", true),
- new EventInfo("weekly3", "weekly from 5/7/2008 on Wed 3 days",
- "2008-05-07T00:00:00", "2008-05-10T00:00:00",
- "FREQ=WEEKLY;BYDAY=WE;WKST=SU", true),
- new EventInfo("weekly4", "weekly from 5/8/2008 on Thu 3 days",
- "2008-05-08T00:00:00", "2008-05-11T00:00:00",
- "FREQ=WEEKLY;BYDAY=TH;WKST=SU", true),
- 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 11:00am to 11:10am",
- "2008-05-01T11:00:00", "2008-05-01T11: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),
- };
-
- private BusyBitInfo[] mBusyBitTests = {
- new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 1,
- new int[] { 0x1 }, new int[] { 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 1,
- new int[] { 0x2 }, new int[] { 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-02T00:00:00", 2,
- new int[] { 0x2, 0x18 }, new int[] { 0, 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 3,
- new int[] { 0x1, 0x2, 0x18 }, new int[] { 0, 0, 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-01T00:00:00", 8,
- new int[] { 0x1, 0x2, 0x18, 0x1f, 0x102, 0x3fe, 0x8da11f, 0xaaaaaa },
- new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-10T00:00:00", 4,
- new int[] { 0xffffff, 0x0, 0x0, 0x0 }, new int[] { 0, 0, 1, 0 } ),
- new BusyBitInfo(mBusyBitEvents, "2008-05-14T00:00:00", 4,
- new int[] { 0x0, 0x2, 0x800400, 0x0 }, new int[] { 3, 2, 1, 0 } ),
-
- // Repeating events
- new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 3,
- new int[] { 0xb01, 0x301, 0x1 }, new int[] { 0, 0, 0 } ),
- new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-01T00:00:00", 10,
- new int[] { 0xb01, 0x301, 0x1, 0x1, 0x1, 0x2031, 0x1, 0x1, 0x1, 0x1 },
- new int[] { 0, 0, 0, 0, 1, 0, 1, 2, 2, 1 } ),
- new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-18T00:00:00", 11,
- new int[] { 0x1, 0x1, 0xa031, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2001, 0x1 },
- new int[] { 0, 1, 0, 1, 2, 2, 1, 0, 1, 0, 1 } ),
- new BusyBitInfo(mBusyBitRepeatingEvents, "2008-05-30T00:00:00", 5,
- new int[] { 0x1, 0x800001, 0x801, 0x1, 0x2031 },
- new int[] { 2, 1, 0, 1, 0 } ),
- };
-
/**
* This sequence of commands inserts and deletes some events.
*/
@@ -789,7 +672,7 @@
new Delete("normal0", 1),
new QueryNumEvents(0),
};
-
+
/**
* This sequence of commands inserts and deletes some all-day events.
*/
@@ -807,7 +690,7 @@
new Delete("allday1", 1),
new QueryNumEvents(0),
};
-
+
/**
* This sequence of commands inserts and deletes some repeating events.
*/
@@ -852,7 +735,8 @@
"2008-05-20T13:00:00", }),
new Insert("cancel0"),
new Update("cancel0", new KeyValue[] {
- new KeyValue(Calendar.EventsColumns.STATUS, "" + Calendar.EventsColumns.STATUS_CANCELED),
+ new KeyValue(Calendar.EventsColumns.STATUS,
+ "" + Calendar.EventsColumns.STATUS_CANCELED),
}),
new VerifyAllInstances("2008-05-01T00:00:00", "2008-05-22T00:01:00",
new String[] {"2008-05-06T13:00:00",
@@ -885,26 +769,26 @@
// 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"}),
+ "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"}),
-
+ "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"}),
+ "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[] {
@@ -926,12 +810,12 @@
* is not present.
*/
private Command[] mExceptionWithNoRecurrence = {
- new Insert("except0"),
+ 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++) {
@@ -943,8 +827,8 @@
return null;
}
- public CalendarProviderTest() {
- super(CalendarProvider.class, Calendar.AUTHORITY);
+ public CalendarProvider2Test() {
+ super(CalendarProvider2ForTesting.class, Calendar.AUTHORITY);
}
@Override
@@ -956,7 +840,9 @@
mResolver.addProvider("subscribedfeeds", new MockProvider("subscribedfeeds"));
mResolver.addProvider("sync", new MockProvider("sync"));
- mDb = getProvider().getDatabase();
+ CalendarDatabaseHelper helper = (CalendarDatabaseHelper) getProvider().getDatabaseHelper();
+ helper.wipeData();
+ mDb = helper.getWritableDatabase();
mMetaData = getProvider().mMetaData;
}
@@ -964,6 +850,7 @@
protected void tearDown() throws Exception {
mDb.close();
mDb = null;
+ getProvider().getDatabaseHelper().close();
super.tearDown();
}
@@ -989,20 +876,26 @@
}
private int insertCal(String name, String timezone) {
+ return insertCal(name, timezone, "joe@joe.com");
+ }
+
+ private int insertCal(String name, String timezone, String account) {
ContentValues m = new ContentValues();
m.put(Calendars.NAME, name);
m.put(Calendars.DISPLAY_NAME, name);
m.put(Calendars.COLOR, "0xff123456");
m.put(Calendars.TIMEZONE, timezone);
m.put(Calendars.SELECTED, 1);
- m.put(Calendars.URL, "http://www.google.com/calendar/feeds/joe%40joe.com/private/full");
- m.put(Calendars.OWNER_ACCOUNT, "joe@joe.com");
+ m.put(Calendars.URL, CALENDAR_URL);
+ m.put(Calendars.OWNER_ACCOUNT, account);
+ m.put(Calendars._SYNC_ACCOUNT, account);
+ m.put(Calendars._SYNC_ACCOUNT_TYPE, "com.google");
- Uri url = mResolver.insert(Uri.parse("content://calendar/calendars"), m);
+ Uri url = mResolver.insert(Calendar.Calendars.CONTENT_URI, m);
String id = url.getLastPathSegment();
return Integer.parseInt(id);
}
-
+
private Uri insertEvent(int calId, EventInfo event) {
if (mWipe) {
// Wipe instance table so it will be regenerated
@@ -1013,7 +906,7 @@
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) {
// This is a normal event
m.put(Events.DTEND, event.mDtend);
@@ -1022,7 +915,7 @@
m.put(Events.RRULE, event.mRrule);
m.put(Events.DURATION, event.mDuration);
}
-
+
if (event.mDescription != null) {
m.put(Events.DESCRIPTION, event.mDescription);
}
@@ -1040,7 +933,7 @@
m.put(Events.ORIGINAL_INSTANCE_TIME, event.mOriginalInstance);
}
Uri url = mResolver.insert(mEventsUri, m);
-
+
// 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.
@@ -1051,7 +944,7 @@
return url;
}
-
+
/**
* Deletes all the events that match the given title.
* @param title the given title to match events on
@@ -1063,13 +956,15 @@
int numRows = 0;
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
- Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, id);
+ // 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);
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
@@ -1091,7 +986,7 @@
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)
@@ -1106,7 +1001,7 @@
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);
}
@@ -1137,13 +1032,12 @@
cursor.close();
return numRows;
}
-
+
private void deleteAllEvents() {
mDb.execSQL("DELETE FROM Events;");
mMetaData.clearInstanceRange();
}
- @LargeTest
public void testInsertNormalEvents() throws Exception {
Cursor cursor;
Uri url = null;
@@ -1156,11 +1050,11 @@
// Keep track of the number of normal events
int numEvents = 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;
for (int ii = 0; ii < len; ii++) {
EventInfo event = mEvents[ii];
@@ -1192,7 +1086,7 @@
cursor = mResolver.query(mEventsUri, null, null, null, null);
assertEquals(numEvents, cursor.getCount());
cursor.close();
-
+
// Check that the Instances table has one instance of each of the
// normal events.
cursor = queryInstances(begin, end);
@@ -1200,7 +1094,6 @@
cursor.close();
}
- @LargeTest
public void testInsertRepeatingEvents() throws Exception {
Cursor cursor;
Uri url = null;
@@ -1236,8 +1129,6 @@
cursor.close();
}
- // TODO: flaky test, temporarily remove from continuous.
- // @LargeTest
public void testInstanceRange() throws Exception {
Cursor cursor;
Uri url = null;
@@ -1263,113 +1154,13 @@
}
assertEquals(instance.mExpectedOccurrences, cursor.getCount());
cursor.close();
- int rows = mResolver.delete(url, null /* selection */, null /* selection args */);
+ // Delete as sync_adapter so event is really deleted.
+ int rows = mResolver.delete(updatedUri(url, true),
+ null /* selection */, null /* selection args */);
assertEquals(1, rows);
}
}
-
- @LargeTest
- public void testBusyBitRange() 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 = mBusyBitTests.length;
- for (int ii = 0; ii < len; ii++) {
- deleteAllEvents();
- BusyBitInfo busyInfo = mBusyBitTests[ii];
- EventInfo[] events = busyInfo.mEvents;
- int numEvents = events.length;
- for (int jj = 0; jj < numEvents; jj++) {
- EventInfo event = events[jj];
- insertEvent(calId, event);
- }
-
- int startDay = busyInfo.mStartDay;
- int numDays = busyInfo.mNumDays;
- int[] busybits = new int[numDays];
- int[] allDayCounts = new int[numDays];
-
- if (false) {
- cursor = mResolver.query(mEventsUri, null, null, null, null);
- Log.i(TAG, "Dump of Events table, count: " + cursor.getCount());
- dumpCursor(cursor);
- cursor.close();
-
- Time time = new Time();
- time.setJulianDay(startDay);
- long begin = time.toMillis(true);
- int endDay = startDay + numDays - 1;
- time.setJulianDay(endDay);
- long end = time.toMillis(true);
- cursor = queryInstances(begin, end);
- Log.i(TAG, "Dump of Instances table, count: " + cursor.getCount()
- + " startDay: " + startDay + " endDay: " + endDay
- + " begin: " + begin + " end: " + end);
- dumpCursor(cursor);
- cursor.close();
- }
-
- cursor = queryBusyBits(startDay, numDays);
- try {
- int dayColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.DAY);
- int busybitColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.BUSYBITS);
- int allDayCountColumnIndex = cursor.getColumnIndexOrThrow(BusyBits.ALL_DAY_COUNT);
-
- while (cursor.moveToNext()) {
- int day = cursor.getInt(dayColumnIndex);
- int dayIndex = day - startDay;
- busybits[dayIndex] = cursor.getInt(busybitColumnIndex);
- allDayCounts[dayIndex] = cursor.getInt(allDayCountColumnIndex);
- }
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
-
- // Compare the database busy bits with the expected busy bits
- for (int dayIndex = 0; dayIndex < numDays; dayIndex++) {
- if (busyInfo.mBusyBits[dayIndex] != busybits[dayIndex]) {
- String mesg = String.format("Test failed!"
- + " BusyBit test index: %d"
- + " day index: %d"
- + " mStartDay: %d mNumDays: %d"
- + " expected busybits: 0x%x was: 0x%x",
- ii, dayIndex, busyInfo.mStartDay, busyInfo.mNumDays,
- busyInfo.mBusyBits[dayIndex], busybits[dayIndex]);
- Log.e(TAG, mesg);
-
- cursor = mResolver.query(mEventsUri, null, null, null, null);
- Log.i(TAG, "Dump of Events table, count: " + cursor.getCount());
- dumpCursor(cursor);
- cursor.close();
- }
- assertEquals(busyInfo.mBusyBits[dayIndex], busybits[dayIndex]);
- }
-
- // Compare the database all-day counts with the expected all-day counts
- for (int dayIndex = 0; dayIndex < numDays; dayIndex++) {
- if (busyInfo.mAllDayCounts[dayIndex] != allDayCounts[dayIndex]) {
- String mesg = String.format("Test failed!"
- + " BusyBit test index: %d"
- + " day index: %d"
- + " expected all-day count: %d was: %d",
- ii, dayIndex,
- busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]);
- Log.e(TAG, mesg);
- }
- assertEquals(busyInfo.mAllDayCounts[dayIndex], allDayCounts[dayIndex]);
- }
- }
- }
-
- @LargeTest
public void testEntityQuery() throws Exception {
testInsertNormalEvents(); // To initialize
@@ -1403,44 +1194,139 @@
attendee.put(Calendar.Attendees.EVENT_ID, 3);
mResolver.insert(Calendar.Attendees.CONTENT_URI, attendee);
- EntityIterator ei = mResolver.queryEntities(mEventsUri, null, null, null);
+ EntityIterator ei = EventsEntity.newEntityIterator(
+ mResolver.query(EventsEntity.CONTENT_URI, null, null, null, null), mResolver);
int count = 0;
- while (ei.hasNext()) {
- Entity entity = ei.next();
- ContentValues values = entity.getEntityValues();
- ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
- switch (values.getAsInteger("_id")) {
- case 1:
- assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties
- break;
- case 2:
- assertEquals(1, subvalues.size()); // Extended properties
- break;
- case 3:
- assertEquals(1, subvalues.size()); // Attendees
- break;
- default:
- assertEquals(0, subvalues.size());
- break;
+ try {
+ while (ei.hasNext()) {
+ Entity entity = ei.next();
+ ContentValues values = entity.getEntityValues();
+ assertEquals(CALENDAR_URL, values.getAsString(Calendars.URL));
+ ArrayList<Entity.NamedContentValues> subvalues = entity.getSubValues();
+ switch (values.getAsInteger("_id")) {
+ case 1:
+ assertEquals(4, subvalues.size()); // 2 x reminder, 2 x extended properties
+ break;
+ case 2:
+ assertEquals(1, subvalues.size()); // Extended properties
+ ContentValues subContentValues = subvalues.get(0).values;
+ String name = subContentValues.getAsString(
+ Calendar.ExtendedProperties.NAME);
+ String value = subContentValues.getAsString(
+ Calendar.ExtendedProperties.VALUE);
+ assertEquals("foo", name);
+ assertEquals("bar", value);
+ break;
+ case 3:
+ assertEquals(1, subvalues.size()); // Attendees
+ break;
+ default:
+ assertEquals(0, subvalues.size());
+ break;
+ }
+ count += 1;
}
- count += 1;
+ assertEquals(5, count);
+ } finally {
+ ei.close();
}
- assertEquals(5, count);
- ei = mResolver.queryEntities(mEventsUri, "Events._id = 3", null, null);
- count = 0;
- while (ei.hasNext()) {
- Entity entity = ei.next();
- count += 1;
+ ei = EventsEntity.newEntityIterator(
+ mResolver.query(EventsEntity.CONTENT_URI, null, "_id = 3", null, null),
+ mResolver);
+ try {
+ count = 0;
+ while (ei.hasNext()) {
+ Entity entity = ei.next();
+ count += 1;
+ }
+ assertEquals(1, count);
+ } finally {
+ ei.close();
}
- assertEquals(1, count);
+ }
+
+ 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(Calendar.Calendars.CONTENT_URI, null /* where */, 2);
+ testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 2);
+
+ int deletes = mResolver.delete(Calendar.Calendars.CONTENT_URI,
+ "ownerAccount='user2@google.com'", null /* selectionArgs */);
+
+ assertEquals(1, deletes);
+ // Should have 1 calendar and 1 event
+ testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 1);
+ testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 1);
+
+ deletes = mResolver.delete(Uri.withAppendedPath(Calendar.Calendars.CONTENT_URI,
+ String.valueOf(calendarId0)),
+ null /* selection*/ , null /* selectionArgs */);
+
+ assertEquals(1, deletes);
+ // Should have 0 calendars and 0 events
+ testQueryCount(Calendar.Calendars.CONTENT_URI, null /* where */, 0);
+ testQueryCount(Calendar.Events.CONTENT_URI, null /* where */, 0);
+
+ deletes = mResolver.delete(Calendar.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[] {
+ Calendar.CalendarAlerts._ID, // 0
+ Calendar.CalendarAlerts.TITLE, // 1
+ Calendar.CalendarAlerts.EVENT_LOCATION, // 2
+ Calendar.CalendarAlerts.ALL_DAY, // 3
+ Calendar.CalendarAlerts.BEGIN, // 4
+ Calendar.CalendarAlerts.END, // 5
+ Calendar.CalendarAlerts.EVENT_ID, // 6
+ Calendar.CalendarAlerts.COLOR, // 7
+ Calendar.CalendarAlerts.RRULE, // 8
+ Calendar.CalendarAlerts.HAS_ALARM, // 9
+ Calendar.CalendarAlerts.STATE, // 10
+ Calendar.CalendarAlerts.ALARM_TIME, // 11
+ };
+ testInsertNormalEvents(); // To initialize
+
+ Uri alertUri = Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
+ 2 /* begin */, 3 /* end */, 4 /* alarmTime */, 5 /* minutes */);
+ Calendar.CalendarAlerts.insert(mResolver, 1 /* eventId */,
+ 2 /* begin */, 7 /* end */, 8 /* alarmTime */, 9 /* minutes */);
+
+ // Regular query
+ Cursor cursor = mResolver.query(Calendar.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(Calendar.CalendarAlerts.CONTENT_URI_BY_INSTANCE, projection,
+ null /* selection */, null /* selectionArgs */, null /* sortOrder */);
+
+ assertEquals(1, cursor.getCount());
+ cursor.close();
}
/**
* Test attendee processing
* @throws Exception
*/
- @LargeTest
public void testAttendees() throws Exception {
mCalendarId = insertCal("Calendar0", DEFAULT_TIMEZONE);
@@ -1496,13 +1382,258 @@
selfAttendeeStatus = cursor.getInt(selfColumn);
assertEquals(Calendar.Attendees.ATTENDEE_STATUS_DECLINED, selfAttendeeStatus);
cursor.close();
+ }
- // This sleep is a big hack. The problem is that CalendarProvider has a TimezoneChecker
- // thread running in the background, and if the test finishes before TimezoneChecker
- // finishes, the thread will try to access the closed database and mess it up.
- // It appears that testAttendees completes fast enough to trigger this, but the other
- // test cases don't.
- Thread.sleep(1000);
+
+ /**
+ * Test the event's _sync_dirty status and clear it.
+ * @param eventId event to fetch.
+ * @param wanted the wanted _sync_dirty status
+ */
+ private void testAndClearDirty(long eventId, int wanted) {
+ Cursor cursor = mResolver.query(
+ ContentUris.withAppendedId(Calendar.Events.CONTENT_URI, eventId),
+ null, null, null, null);
+ try {
+ assertEquals("Event count", 1, cursor.getCount());
+ cursor.moveToNext();
+ int dirty = cursor.getInt(cursor.getColumnIndex(Calendar.Events._SYNC_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 _sync_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);
+ }
+
+ /**
+ * Add CALLER_IS_SYNCADAPTER to URI if this is a sync adapter operation.
+ */
+ private Uri updatedUri(Uri uri, boolean syncAdapter) {
+ if (syncAdapter) {
+ return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ } 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);
+
+ Uri eventUri = insertEvent(mCalendarId, findEvent("daily0"));
+
+ long eventId = ContentUris.parseId(eventUri);
+ testAndClearDirty(eventId, 1);
+
+ ContentValues attendee = new ContentValues();
+ attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Joe");
+ attendee.put(Calendar.Attendees.ATTENDEE_EMAIL, "joe@joe.com");
+ attendee.put(Calendar.Attendees.ATTENDEE_TYPE, Calendar.Attendees.TYPE_REQUIRED);
+ attendee.put(Calendar.Attendees.ATTENDEE_RELATIONSHIP,
+ Calendar.Attendees.RELATIONSHIP_ORGANIZER);
+ attendee.put(Calendar.Attendees.EVENT_ID, eventId);
+
+ Uri attendeeUri = mResolver.insert(
+ updatedUri(Calendar.Attendees.CONTENT_URI, syncAdapter),
+ attendee);
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
+
+ ContentValues reminder = new ContentValues();
+ reminder.put(Calendar.Reminders.MINUTES, 10);
+ reminder.put(Calendar.Reminders.METHOD, Calendar.Reminders.METHOD_EMAIL);
+ reminder.put(Calendar.Attendees.EVENT_ID, eventId);
+
+ Uri reminderUri = mResolver.insert(
+ updatedUri(Calendar.Reminders.CONTENT_URI, syncAdapter), reminder);
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
+
+ ContentValues alert = new ContentValues();
+ alert.put(Calendar.CalendarAlerts.BEGIN, 10);
+ alert.put(Calendar.CalendarAlerts.END, 20);
+ alert.put(Calendar.CalendarAlerts.ALARM_TIME, 30);
+ alert.put(Calendar.CalendarAlerts.CREATION_TIME, 40);
+ alert.put(Calendar.CalendarAlerts.RECEIVED_TIME, 50);
+ alert.put(Calendar.CalendarAlerts.NOTIFY_TIME, 60);
+ alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.SCHEDULED);
+ alert.put(Calendar.CalendarAlerts.MINUTES, 30);
+ alert.put(Calendar.CalendarAlerts.EVENT_ID, eventId);
+
+ Uri alertUri = mResolver.insert(
+ updatedUri(Calendar.CalendarAlerts.CONTENT_URI, syncAdapter), alert);
+ // Alerts don't dirty the event
+ testAndClearDirty(eventId, 0);
+ testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
+
+ ContentValues extended = new ContentValues();
+ extended.put(Calendar.ExtendedProperties.NAME, "foo");
+ extended.put(Calendar.ExtendedProperties.VALUE, "bar");
+ extended.put(Calendar.ExtendedProperties.EVENT_ID, eventId);
+
+ Uri extendedUri = mResolver.insert(
+ updatedUri(Calendar.ExtendedProperties.CONTENT_URI, syncAdapter), extended);
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
+
+ // Now test updates
+
+ attendee = new ContentValues();
+ attendee.put(Calendar.Attendees.ATTENDEE_NAME, "Sam");
+ // Need to include EVENT_ID with attendee update. Is that desired?
+ attendee.put(Calendar.Attendees.EVENT_ID, eventId);
+
+ assertEquals("update", 1, mResolver.update(updatedUri(attendeeUri, syncAdapter), attendee,
+ null /* where */, null /* selectionArgs */));
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+
+ testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 1);
+
+ reminder = new ContentValues();
+ reminder.put(Calendar.Reminders.MINUTES, 20);
+
+ assertEquals("update", 1, mResolver.update(updatedUri(reminderUri, syncAdapter), reminder,
+ null /* where */, null /* selectionArgs */));
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 1);
+
+ alert = new ContentValues();
+ alert.put(Calendar.CalendarAlerts.STATE, Calendar.CalendarAlerts.DISMISSED);
+
+ assertEquals("update", 1, mResolver.update(updatedUri(alertUri, syncAdapter), alert,
+ null /* where */, null /* selectionArgs */));
+ // Alerts don't dirty the event
+ testAndClearDirty(eventId, 0);
+ testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 1);
+
+ extended = new ContentValues();
+ extended.put(Calendar.ExtendedProperties.VALUE, "baz");
+
+ assertEquals("update", 1, mResolver.update(updatedUri(extendedUri, syncAdapter), extended,
+ null /* where */, null /* selectionArgs */));
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.ExtendedProperties.CONTENT_URI, "event_id=" + eventId, 1);
+
+ // Now test deletes
+
+ assertEquals("delete", 1, mResolver.delete(
+ updatedUri(attendeeUri, syncAdapter),
+ null, null /* selectionArgs */));
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.Attendees.CONTENT_URI, "event_id=" + eventId, 0);
+
+ assertEquals("delete", 1, mResolver.delete(updatedUri(reminderUri, syncAdapter),
+ null /* where */, null /* selectionArgs */));
+
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.Reminders.CONTENT_URI, "event_id=" + eventId, 0);
+
+ assertEquals("delete", 1, mResolver.delete(updatedUri(alertUri, syncAdapter),
+ null /* where */, null /* selectionArgs */));
+
+ // Alerts don't dirty the event
+ testAndClearDirty(eventId, 0);
+ testQueryCount(Calendar.CalendarAlerts.CONTENT_URI, "event_id=" + eventId, 0);
+
+ assertEquals("delete", 1, mResolver.delete(updatedUri(extendedUri, syncAdapter),
+ null /* where */, null /* selectionArgs */));
+
+ testAndClearDirty(eventId, syncAdapter ? 0 : 1);
+ testQueryCount(Calendar.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(Calendar.Events.CONTENT_URI, null, 2);
+
+ assertEquals("delete", 1, mResolver.delete(Calendar.Calendars.CONTENT_URI,
+ "_id=" + mCalendarId, null));
+ // Calendar should be deleted
+ testQueryCount(Calendar.Calendars.CONTENT_URI, null, 0);
+ // Event should be gone
+ testQueryCount(Calendar.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(Calendar.Events.CONTENT_URI, null, 2);
+ Uri eventsWithAccount = Calendar.Events.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_NAME, "joe@joe.com")
+ .appendQueryParameter(Calendar.EventsEntity.ACCOUNT_TYPE, "com.google")
+ .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 */),
+ "_id=" + eventId, null /* selectionArgs */));
+ testQueryCount(Calendar.Events.CONTENT_URI, null, 2);
+ // Right account, should be deleted
+ assertEquals("delete", 1, mResolver.delete(
+ updatedUri(Calendar.Events.CONTENT_URI, true /* syncAdapter */),
+ "_id=" + eventId, null /* selectionArgs */));
+ testQueryCount(Calendar.Events.CONTENT_URI, null, 1);
}
/**
@@ -1510,7 +1641,6 @@
* This tests full instance expansion.
* @throws Exception
*/
- @LargeTest
public void testCommandSequences1() throws Exception {
commandSequences(true);
}
@@ -1520,7 +1650,6 @@
* This tests incremental instance expansion.
* @throws Exception
*/
- @LargeTest
public void testCommandSequences2() throws Exception {
commandSequences(false);
}
@@ -1547,23 +1676,23 @@
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");
@@ -1618,18 +1747,49 @@
time.parse3339(str);
assertEquals(result, time.toString());
}
-
+
+ /**
+ * Test the query done by Event.loadEvents
+ * @throws Exception
+ */
+ public void testInstanceQuery() throws Exception {
+ final String[] PROJECTION = new String[] {
+ Instances.TITLE, // 0
+ Instances.EVENT_LOCATION, // 1
+ Instances.ALL_DAY, // 2
+ Instances.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 = Instances.SORT_CALENDAR_VIEW;
+ String where = Instances.SELF_ATTENDEE_STATUS + "!=" + Calendar.Attendees.ATTENDEE_STATUS_DECLINED;
+
+ long start = 0;
+ long end = 0;
+ Cursor c = Instances.query(mResolver, PROJECTION,
+ start - DateUtils.DAY_IN_MILLIS, end + DateUtils.DAY_IN_MILLIS, where, orderBy);
+ // Just make sure the query doesn't crash. TODO: could check results
+ }
+
private Cursor queryInstances(long begin, long end) {
- Uri url = Uri.parse("content://calendar/instances/when/" + begin + "/" + end);
+ Uri url = Uri.withAppendedPath(Calendar.Instances.CONTENT_URI, begin + "/" + end);
return mResolver.query(url, null, null, null, null);
}
- private Cursor queryBusyBits(int startDay, int numDays) {
- int endDay = startDay + numDays - 1;
- Uri url = Uri.parse("content://calendar/busybits/when/" + startDay + "/" + endDay);
- return mResolver.query(url, null, null, null, null);
- }
-
protected static class MockProvider extends ContentProvider {
private String mAuthority;
@@ -1673,4 +1833,96 @@
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://" + Calendar.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(Calendar.Events.TITLE);
+ int colIndexDtStart = cursor.getColumnIndex(Calendar.Events.DTSTART);
+ int colIndexDtEnd = cursor.getColumnIndex(Calendar.Events.DTEND);
+ int colIndexAllDay = cursor.getColumnIndex(Calendar.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(Calendar.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);
+
+// TODO (fdimeglio): uncomment when the VM is more stable
+// // check timezone database version
+// assertEquals(TimeUtils.getTimeZoneDatabaseVersion(),
+// getProvider().getTimezoneDatabaseVersion());
+
+ // 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 if the inserted event as been updated with the timezone information
+ // there is 1h time difference between America/LosAngeles and America/Anchorage
+ long deltaMillisForTimezones = 3600000L;
+ checkEvent(1, events[0].mTitle,
+ events[0].mDtstart + deltaMillisForTimezones,
+ events[0].mDtend + deltaMillisForTimezones,
+ events[0].mAllDay);
+ }
}
diff --git a/tests/src/com/android/providers/calendar/CalendarSyncTestingBase.java b/tests/src/com/android/providers/calendar/CalendarSyncTestingBase.java
index fb54dee..478fb8d 100644
--- a/tests/src/com/android/providers/calendar/CalendarSyncTestingBase.java
+++ b/tests/src/com/android/providers/calendar/CalendarSyncTestingBase.java
@@ -32,7 +32,7 @@
import android.util.Log;
import com.google.android.collect.Maps;
-import com.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.gsf.GoogleLoginServiceConstants;
import java.util.HashSet;
import java.util.Map;
@@ -43,7 +43,7 @@
protected Context mTargetContext;
protected String mAccount;
protected ContentResolver mResolver;
- protected Uri mEventsUri = Uri.parse("content://calendar/events");
+ protected Uri mEventsUri = Calendar.Events.CONTENT_URI;
static final String TAG = "calendar";
static final String DEFAULT_TIMEZONE = "America/Los_Angeles";
diff --git a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java b/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
deleted file mode 100644
index 01d969e..0000000
--- a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/* //device/content/providers/pim/TestCalendarSyncAdapter.java
-**
-** Copyright 2006, 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,
-** See the License for the specific language governing permissions and
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** limitations under the License.
-*/
-
-package com.android.providers.calendar;
-
-import android.content.ContentValues;
-import android.content.SyncableContentProvider;
-import android.content.SyncContext;
-import android.content.SyncResult;
-import android.content.TempProviderSyncAdapter;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.format.DateUtils;
-import android.accounts.Account;
-import android.accounts.AuthenticatorException;
-import android.accounts.OperationCanceledException;
-
-import java.util.Calendar;
-import java.util.TimeZone;
-import java.io.IOException;
-
-
-public class TestCalendarSyncAdapter extends TempProviderSyncAdapter {
- private static Uri sEventsURL = Uri.parse("content://calendar/events/");
- private static Uri sDeletedEventsURL = Uri.parse("content://calendar/deleted_events/");
-
- public TestCalendarSyncAdapter(SyncableContentProvider provider) {
- super(provider);
- }
-
- @Override
- public void onSyncStarting(SyncContext context, Account account, boolean manualSync,
- SyncResult result)
- {
- }
-
- @Override
- public void onSyncEnding(SyncContext context, boolean success)
- {
- }
-
- @Override
- public boolean isReadOnly()
- {
- return false;
- }
-
- public boolean getIsSyncable(Account account)
- throws IOException, AuthenticatorException, OperationCanceledException {
- return true;
- }
-
- @Override
- public void getServerDiffs(SyncContext context, SyncData syncData,
- SyncableContentProvider tempProvider,
- Bundle extras, Object syncInfo, SyncResult syncResult) {
- switch(sSyncClock) {
- case 1: {
- ContentValues values = new ContentValues();
- Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
-
- values.put("title", "Sync performed");
- values.put("dtstart", DateUtils.writeDateTime(cal, true));
- values.put("duration", "PT1S");
- values.put("_sync_id", "server_event_1");
- values.put("_sync_time", Long.valueOf(System.currentTimeMillis()));
-
- tempProvider.insert(sEventsURL, values);
- break;
- }
-
- case 2: {
- ContentValues values = new ContentValues();
- Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
- cal.clear();
- cal.set(1979, Calendar.APRIL, 6, 0, 0);
-
- values.put("title", "Jeff's b-day");
- values.put("dtstart", DateUtils.writeDateTime(cal, true));
- values.put("duration", "PT1D");
- values.put("_sync_id", "server_event_2");
- values.put("_sync_time", Long.valueOf(System.currentTimeMillis()));
-
- tempProvider.insert(sEventsURL, values);
- break;
- }
-
- case 3: {
- ContentValues values = new ContentValues();
- Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
- cal.clear();
- cal.set(1979, Calendar.APRIL, 6, 0, 0);
-
- values.put("title", "Jeff's Birthday");
- values.put("dtstart", DateUtils.writeDateTime(cal, true));
- values.put("duration", "PT1D");
- values.put("_sync_id", "server_event_2");
- values.put("_sync_time", Long.valueOf(System.currentTimeMillis()));
-
- tempProvider.insert(sEventsURL, values);
- break;
- }
-
- case 4: {
- ContentValues values = new ContentValues();
- values.put("_sync_id", "server_event_1");
- tempProvider.insert(sDeletedEventsURL, values);
- break;
- }
-
- case 5: {
- ContentValues values = new ContentValues();
- Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
- cal.clear();
- cal.set(1979, Calendar.APRIL, 6, 0, 0);
-
- values.put("title", "Jeff Hamilton's Birthday");
- values.put("dtstart", DateUtils.writeDateTime(cal, true));
- values.put("duration", "PT1D");
- values.put("_sync_id", "server_event_2");
- values.put("_sync_time", Long.valueOf(System.currentTimeMillis()));
-
- tempProvider.insert(sEventsURL, values);
- break;
- }
- }
-
- sSyncClock++;
- }
-
- @Override
- public void sendClientDiffs(SyncContext context,
- SyncableContentProvider clientDiffs,
- SyncableContentProvider serverDiffs, SyncResult syncResult,
- boolean dontActuallySendDeletes) {
- Cursor cursor = clientDiffs.query(sEventsURL, null, null, null, null);
- if (cursor == null || cursor.getCount() == 0) {
- throw new IllegalStateException("Empty client diffs");
- }
-
- int syncIDColumn = cursor.getColumnIndex("_sync_id");
- int syncTimeColumn = cursor.getColumnIndex("_sync_time");
- while(cursor.moveToNext()) {
- cursor.updateString(syncIDColumn, "client_event_" + mClientEventID++);
- cursor.updateLong(syncTimeColumn, System.currentTimeMillis());
- }
- cursor.commitUpdates();
- cursor.deactivate();
- }
-
- private int mClientEventID = 1;
- private static int sSyncClock = 1;
-
- @Override
- public void onSyncCanceled() {
- throw new UnsupportedOperationException("not implemented");
- }
-
- public void onAccountsChanged(Account[] accounts) {
- }
-}
-
-
diff --git a/tests/src/com/android/providers/calendar/VCalTest.java b/tests/src/com/android/providers/calendar/VCalTest.java
deleted file mode 100644
index 895b1a1..0000000
--- a/tests/src/com/android/providers/calendar/VCalTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/* //device/content/providers/pim/VCalTest.java
-**
-** Copyright 2006, 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 junit.framework.TestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-
-public class VCalTest extends TestCase {
-
- @SmallTest
- public void testParse1() throws Exception {
- String str =
- "DTSTART;TZID=\"America/Los_Angeles\":20060908T170000\r\n"
- + "DURATION;X-TEST=joe;X-Test=\"http://joe;\":PT3600S\r\n"
- + "RRULE:FREQ=WEEKLY;BYDAY=FR;WKST=SU\r\n"
- + "BEGIN:VTIMEZONE\r\n"
- + "TZID:America/Los_Angeles\r\n"
- + "X-LIC-LOCATION:America/Los_Angeles\r\n"
- + "BEGIN:STANDARD\r\n"
- + "TZOFFSETFROM:-0700\r\n"
- + "TZOFFSETTO:-0800\r\n"
- + "TZNAME:PST\r\n"
- + "DTSTART:19701025T020000\r\n"
- + "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\n"
- + "END:STANDARD\r\n"
- + "BEGIN:DAYLIGHT\r\n"
- + "TZOFFSETFROM:-0800\r\n"
- + "TZOFFSETTO:-0700\r\n"
- + "TZNAME:PDT\r\n"
- + "DTSTART:19700405T020000\r\n"
- + "RRULE:\r\n"
- + " FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\n"
- + "END:DAYLIGHT\r\n"
- + "END:VTIMEZONE\r\n";
- VCal.parse(str);
- }
-}
-
-