am ba79889d: (-s ours) Disallow empty eventTimezone values. Do not merge

* commit 'ba79889dd64ed6ab2ca8d25d97c30e6168f55f6b':
  Disallow empty eventTimezone values. Do not merge
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
new file mode 100644
index 0000000..9fe4148
--- /dev/null
+++ b/res/values-be/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="calendar_storage" msgid="5387668002987562770">"Сховішча каляндара"</string>
+    <string name="upgrade_msg" msgid="2792831029435070926">"Абнаўленне базы дадзеных Calendar."</string>
+    <string name="calendar_default_name" msgid="6924293766625167275">"Па змаўчанні"</string>
+    <string name="calendar_info" msgid="6687678621418059281">"Інфармацыя пра каляндар"</string>
+    <string name="calendar_info_error" msgid="5575162446528419982">"Памылка"</string>
+    <string name="calendar_info_no_calendars" msgid="4287534468186704433">"Няма календароў"</string>
+    <string name="calendar_info_events" msgid="1805502308105103803">"Мерапрыемствы: <xliff:g id="EVENTS">%1$d</xliff:g>"</string>
+    <string name="calendar_info_events_dirty" msgid="8879392112564499515">"Мерапрыемствы: <xliff:g id="EVENTS_0">%1$d</xliff:g>; незахаваных: <xliff:g id="DIRTY_EVENTS">%2$d</xliff:g>"</string>
+    <string name="provider_label" msgid="2306513350843464739">"Calendar"</string>
+    <string name="debug_tool_delete_button" msgid="5052706251268452090">"Выдаліць"</string>
+    <string name="debug_tool_start_button" msgid="5384780896342913563">"Пачаць"</string>
+    <string name="debug_tool_message" msgid="4414152820946316089">"Вы збіраецеся: 1) зрабіць копію базы дадзеных свайго календара на SD-карту ці USB-назапашвальнiк, якія можа прачытаць любое прыкладанне, і 2) адправіць яе па электроннай пошце. Абавязкова выдаліце яе, як толькі яна будзе паспяхова скапіявана з прылады ці будзе атрыманы электронны ліст."</string>
+    <string name="debug_tool_email_sender_picker" msgid="4418621476577347562">"Выберыце праграму для адпраўкі файла"</string>
+    <string name="debug_tool_email_subject" msgid="2403590332256471194">"Каляндар Db далучаны"</string>
+    <string name="debug_tool_email_body" msgid="740309162644398319">"Укладзена база дадзеных майго календара з усімі сустрэчамі і асабістай інфармацыяй. Працуйце з ёй уважліва."</string>
+</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
new file mode 100644
index 0000000..55e28a5
--- /dev/null
+++ b/res/values-et/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="calendar_storage" msgid="5387668002987562770">"Kalendri mälumaht"</string>
+    <string name="upgrade_msg" msgid="2792831029435070926">"Kalendri andmebaasi uuendamine."</string>
+    <string name="calendar_default_name" msgid="6924293766625167275">"Vaikimisi"</string>
+    <string name="calendar_info" msgid="6687678621418059281">"Kalendri teave"</string>
+    <string name="calendar_info_error" msgid="5575162446528419982">"Viga"</string>
+    <string name="calendar_info_no_calendars" msgid="4287534468186704433">"Kalendrid puuduvad"</string>
+    <string name="calendar_info_events" msgid="1805502308105103803">"Sündmused: <xliff:g id="EVENTS">%1$d</xliff:g>"</string>
+    <string name="calendar_info_events_dirty" msgid="8879392112564499515">"Sündmused: <xliff:g id="EVENTS_0">%1$d</xliff:g>, salvestamata: <xliff:g id="DIRTY_EVENTS">%2$d</xliff:g>"</string>
+    <string name="provider_label" msgid="2306513350843464739">"Kalender"</string>
+    <string name="debug_tool_delete_button" msgid="5052706251268452090">"Kustuta kohe"</string>
+    <string name="debug_tool_start_button" msgid="5384780896342913563">"Alusta"</string>
+    <string name="debug_tool_message" msgid="4414152820946316089">"Olete 1) tegemas koopiat oma kalendri andmebaasist SD-kaardile/USB-mäluruumi, mida saavad lugeda kõik rakendused, ja 2) seda meilimas. Kustutage see kindlasti kohe pärast seadmest kustutamist või meili kättesaamist."</string>
+    <string name="debug_tool_email_sender_picker" msgid="4418621476577347562">"Vali programm faili saatmiseks"</string>
+    <string name="debug_tool_email_subject" msgid="2403590332256471194">"Kalendri Db on lisatud"</string>
+    <string name="debug_tool_email_body" msgid="740309162644398319">"Lisatud on minu kalendri andmebaas koos kõigi kohtumiste ja isiklike andmetega. Käsitlege neid ettevaatlikult."</string>
+</resources>
diff --git a/src/com/android/providers/calendar/CalendarDatabaseHelper.java b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
index 586515c..d609bdf 100644
--- a/src/com/android/providers/calendar/CalendarDatabaseHelper.java
+++ b/src/com/android/providers/calendar/CalendarDatabaseHelper.java
@@ -28,6 +28,8 @@
 import android.os.Bundle;
 import android.provider.CalendarContract;
 import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Colors;
 import android.provider.CalendarContract.Events;
 import android.provider.CalendarContract.Reminders;
 import android.provider.SyncStateContract;
@@ -60,7 +62,7 @@
     // Versions under 100 cover through Froyo, 1xx version are for Gingerbread,
     // 2xx for Honeycomb, and 3xx for ICS. For future versions bump this to the
     // next hundred at each major release.
-    static final int DATABASE_VERSION = 307;
+    static final int DATABASE_VERSION = 308;
 
     private static final int PRE_FROYO_SYNC_STATE_VERSION = 3;
 
@@ -72,6 +74,7 @@
             Events.EVENT_LOCATION + "," +
             Events.DESCRIPTION + "," +
             Events.EVENT_COLOR + "," +
+            Events.EVENT_COLOR_KEY + "," +
             Events.STATUS + "," +
             Events.SELF_ATTENDEE_STATUS + "," +
             Events.DTSTART + "," +
@@ -130,6 +133,7 @@
         public static final String CALENDAR_CACHE = "CalendarCache";
         public static final String SYNC_STATE = "_sync_state";
         public static final String SYNC_STATE_META = "_sync_state_metadata";
+        public static final String COLORS = "Colors";
     }
 
     public interface Views {
@@ -179,6 +183,33 @@
             " WHERE " + CalendarContract.Events.CALENDAR_ID + "=" +
                 "old." + CalendarContract.Events._ID + ";";
 
+    private static final String CALENDAR_UPDATE_COLOR_TRIGGER_SQL = "UPDATE " + Tables.CALENDARS
+            + " SET calendar_color=(SELECT " + Colors.COLOR + " FROM " + Tables.COLORS + " WHERE "
+            + Colors.ACCOUNT_NAME + "=" + "new." + Calendars.ACCOUNT_NAME + " AND "
+            + Colors.ACCOUNT_TYPE + "=" + "new." + Calendars.ACCOUNT_TYPE + " AND "
+            + Colors.COLOR_KEY + "=" + "new." + Calendars.CALENDAR_COLOR_KEY + ") "
+            + " WHERE " + Calendars._ID + "=" + "old." + Calendars._ID
+            + ";";
+    private static final String CALENDAR_COLOR_UPDATE_TRIGGER_NAME = "calendar_color_update";
+    private static final String CREATE_CALENDAR_COLOR_UPDATE_TRIGGER = "CREATE TRIGGER "
+            + CALENDAR_COLOR_UPDATE_TRIGGER_NAME + " UPDATE OF " + Calendars.CALENDAR_COLOR_KEY
+            + " ON " + Tables.CALENDARS + " WHEN new." + Calendars.CALENDAR_COLOR_KEY
+            + " NOT NULL BEGIN " + CALENDAR_UPDATE_COLOR_TRIGGER_SQL + " END";
+
+    private static final String EVENT_UPDATE_COLOR_TRIGGER_SQL = "UPDATE " + Tables.EVENTS
+            + " SET eventColor=(SELECT " + Colors.COLOR + " FROM " + Tables.COLORS + " WHERE "
+            + Colors.ACCOUNT_NAME + "=" + "(SELECT " + Calendars.ACCOUNT_NAME + " FROM "
+            + Tables.CALENDARS + " WHERE " + Calendars._ID + "=new." + Events.CALENDAR_ID
+            + ") AND " + Colors.ACCOUNT_TYPE + "=" + "(SELECT " + Calendars.ACCOUNT_TYPE + " FROM "
+            + Tables.CALENDARS + " WHERE " + Calendars._ID + "=new." + Events.CALENDAR_ID
+            + ") AND " + Colors.COLOR_KEY + "=" + "new." + Events.EVENT_COLOR_KEY + ") "
+            + " WHERE " + Events._ID + "=" + "old." + Events._ID + ";";
+    private static final String EVENT_COLOR_UPDATE_TRIGGER_NAME = "event_color_update";
+    private static final String CREATE_EVENT_COLOR_UPDATE_TRIGGER = "CREATE TRIGGER "
+            + EVENT_COLOR_UPDATE_TRIGGER_NAME + " UPDATE OF " + Events.EVENT_COLOR_KEY + " ON "
+            + Tables.EVENTS + " WHEN new." + Events.EVENT_COLOR_KEY + " NOT NULL BEGIN "
+            + EVENT_UPDATE_COLOR_TRIGGER_SQL + " END";
+
     /** Selects rows from Attendees for which the event_id refers to a nonexistent Event */
     private static final String WHERE_ATTENDEES_ORPHANS =
             Attendees.EVENT_ID + " IN (SELECT " + Attendees.EVENT_ID + " FROM " +
@@ -200,6 +231,7 @@
     private static CalendarDatabaseHelper sSingleton = null;
 
     private DatabaseUtils.InsertHelper mCalendarsInserter;
+    private DatabaseUtils.InsertHelper mColorsInserter;
     private DatabaseUtils.InsertHelper mEventsInserter;
     private DatabaseUtils.InsertHelper mEventsRawTimesInserter;
     private DatabaseUtils.InsertHelper mInstancesInserter;
@@ -212,6 +244,10 @@
         return mCalendarsInserter.insert(values);
     }
 
+    public long colorsInsert(ContentValues values) {
+        return mColorsInserter.insert(values);
+    }
+
     public long eventsInsert(ContentValues values) {
         return mEventsInserter.insert(values);
     }
@@ -271,6 +307,7 @@
         mSyncState.onDatabaseOpened(db);
 
         mCalendarsInserter = new DatabaseUtils.InsertHelper(db, Tables.CALENDARS);
+        mColorsInserter = new DatabaseUtils.InsertHelper(db, Tables.COLORS);
         mEventsInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS);
         mEventsRawTimesInserter = new DatabaseUtils.InsertHelper(db, Tables.EVENTS_RAW_TIMES);
         mInstancesInserter = new DatabaseUtils.InsertHelper(db, Tables.INSTANCES);
@@ -333,6 +370,8 @@
 
         mSyncState.createDatabase(db);
 
+        createColorsTable(db);
+
         createCalendarsTable(db);
 
         createEventsTable(db);
@@ -441,6 +480,10 @@
                 EVENTS_CLEANUP_TRIGGER_SQL +
                 "END");
 
+        // Triggers to update the color stored in an event or a calendar when
+        // the color_index is changed.
+        createColorsTriggers(db);
+
         // Trigger to update exceptions when an original event updates its
         // _sync_id
         db.execSQL(CREATE_SYNC_ID_UPDATE_TRIGGER);
@@ -464,6 +507,7 @@
                 CalendarContract.Events.EVENT_LOCATION + " TEXT," +
                 CalendarContract.Events.DESCRIPTION + " TEXT," +
                 CalendarContract.Events.EVENT_COLOR + " INTEGER," +
+                CalendarContract.Events.EVENT_COLOR_KEY + " TEXT," +
                 CalendarContract.Events.STATUS + " INTEGER," +
                 CalendarContract.Events.SELF_ATTENDEE_STATUS + " INTEGER NOT NULL DEFAULT 0," +
                 // dtstart in millis since epoch
@@ -516,6 +560,68 @@
                 + CalendarContract.Events.CALENDAR_ID + ");");
     }
 
+    private void createEventsTable307(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE Events ("
+                + "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+                + "_sync_id TEXT,"
+                + "dirty INTEGER,"
+                + "lastSynced INTEGER DEFAULT 0,"
+                + "calendar_id INTEGER NOT NULL,"
+                + "title TEXT,"
+                + "eventLocation TEXT,"
+                + "description TEXT,"
+                + "eventColor INTEGER,"
+                + "eventStatus INTEGER,"
+                + "selfAttendeeStatus INTEGER NOT NULL DEFAULT 0,"
+                // dtstart in millis since epoch
+                + "dtstart INTEGER,"
+                // dtend in millis since epoch
+                + "dtend INTEGER,"
+                // timezone for event
+                + "eventTimezone TEXT,"
+                + "duration TEXT,"
+                + "allDay INTEGER NOT NULL DEFAULT 0,"
+                + "accessLevel INTEGER NOT NULL DEFAULT 0,"
+                + "availability 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,"
+                + "original_id INTEGER,"
+                // ORIGINAL_SYNC_ID is the _sync_id of recurring event
+                + "original_sync_id TEXT,"
+                // originalInstanceTime is in millis since epoch
+                + "originalInstanceTime INTEGER,"
+                + "originalAllDay INTEGER,"
+                // lastDate is in millis since epoch
+                + "lastDate INTEGER,"
+                + "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,"
+                // timezone for event with allDay events are in local timezone
+                + "eventEndTimezone TEXT,"
+                // SYNC_DATAX columns are available for use by sync adapters
+                + "sync_data1 TEXT,"
+                + "sync_data2 TEXT,"
+                + "sync_data3 TEXT,"
+                + "sync_data4 TEXT,"
+                + "sync_data5 TEXT,"
+                + "sync_data6 TEXT,"
+                + "sync_data7 TEXT,"
+                + "sync_data8 TEXT,"
+                + "sync_data9 TEXT,"
+                + "sync_data10 TEXT);");
+
+        // **When updating this be sure to also update LAST_SYNCED_EVENT_COLUMNS
+
+        db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (calendar_id);");
+    }
+
     // TODO Remove this method after merging all ICS upgrades
     private void createEventsTable300(SQLiteDatabase db) {
         db.execSQL("CREATE TABLE Events (" +
@@ -611,6 +717,24 @@
                 "END");
     }
 
+    private void createColorsTable(SQLiteDatabase db) {
+
+        db.execSQL("CREATE TABLE " + Tables.COLORS + " (" +
+                CalendarContract.Colors._ID + " INTEGER PRIMARY KEY," +
+                CalendarContract.Colors.ACCOUNT_NAME + " TEXT NOT NULL," +
+                CalendarContract.Colors.ACCOUNT_TYPE + " TEXT NOT NULL," +
+                CalendarContract.Colors.DATA + " TEXT," +
+                CalendarContract.Colors.COLOR_TYPE + " INTEGER NOT NULL," +
+                CalendarContract.Colors.COLOR_KEY + " TEXT NOT NULL," +
+                CalendarContract.Colors.COLOR + " INTEGER NOT NULL" +
+                ");");
+    }
+
+    public void createColorsTriggers(SQLiteDatabase db) {
+        db.execSQL(CREATE_EVENT_COLOR_UPDATE_TRIGGER);
+        db.execSQL(CREATE_CALENDAR_COLOR_UPDATE_TRIGGER);
+    }
+
     private void createCalendarsTable(SQLiteDatabase db) {
         db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
                 CalendarContract.Calendars._ID + " INTEGER PRIMARY KEY," +
@@ -621,6 +745,7 @@
                 CalendarContract.Calendars.NAME + " TEXT," +
                 CalendarContract.Calendars.CALENDAR_DISPLAY_NAME + " TEXT," +
                 CalendarContract.Calendars.CALENDAR_COLOR + " INTEGER," +
+                CalendarContract.Calendars.CALENDAR_COLOR_KEY + " TEXT," +
                 CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + " INTEGER," +
                 CalendarContract.Calendars.VISIBLE + " INTEGER NOT NULL DEFAULT 1," +
                 CalendarContract.Calendars.SYNC_EVENTS + " INTEGER NOT NULL DEFAULT 0," +
@@ -632,6 +757,8 @@
                 CalendarContract.Calendars.CAN_PARTIALLY_UPDATE + " INTEGER DEFAULT 0," +
                 CalendarContract.Calendars.MAX_REMINDERS + " INTEGER DEFAULT 5," +
                 CalendarContract.Calendars.ALLOWED_REMINDERS + " TEXT DEFAULT '0,1'," +
+                CalendarContract.Calendars.ALLOWED_AVAILABILITY + " TEXT DEFAULT '0,1'," +
+                CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES + " TEXT DEFAULT '0,1,2'," +
                 CalendarContract.Calendars.DELETED + " INTEGER NOT NULL DEFAULT 0," +
                 CalendarContract.Calendars.CAL_SYNC1 + " TEXT," +
                 CalendarContract.Calendars.CAL_SYNC2 + " TEXT," +
@@ -652,6 +779,47 @@
                 "END");
     }
 
+    private void createCalendarsTable305(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE Calendars (" +
+                "_id INTEGER PRIMARY KEY," +
+                "account_name TEXT," +
+                "account_type TEXT," +
+                "_sync_id TEXT," +
+                "dirty INTEGER," +
+                "name TEXT," +
+                "calendar_displayName TEXT," +
+                "calendar_color INTEGER," +
+                "calendar_access_level INTEGER," +
+                "visible INTEGER NOT NULL DEFAULT 1," +
+                "sync_events INTEGER NOT NULL DEFAULT 0," +
+                "calendar_location TEXT," +
+                "calendar_timezone TEXT," +
+                "ownerAccount TEXT, " +
+                "canOrganizerRespond INTEGER NOT NULL DEFAULT 1," +
+                "canModifyTimeZone INTEGER DEFAULT 1," +
+                "canPartiallyUpdate INTEGER DEFAULT 0," +
+                "maxReminders INTEGER DEFAULT 5," +
+                "allowedReminders TEXT DEFAULT '0,1'," +
+                "deleted INTEGER NOT NULL DEFAULT 0," +
+                "cal_sync1 TEXT," +
+                "cal_sync2 TEXT," +
+                "cal_sync3 TEXT," +
+                "cal_sync4 TEXT," +
+                "cal_sync5 TEXT," +
+                "cal_sync6 TEXT," +
+                "cal_sync7 TEXT," +
+                "cal_sync8 TEXT," +
+                "cal_sync9 TEXT," +
+                "cal_sync10 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");
+    }
+
     private void createCalendarsTable300(SQLiteDatabase db) {
         db.execSQL("CREATE TABLE " + Tables.CALENDARS + " (" +
                 "_id INTEGER PRIMARY KEY," +
@@ -1174,6 +1342,11 @@
                 upgradeToVersion307(db);
                 oldVersion++;
             }
+            if (oldVersion == 307) {
+                upgradeToVersion308(db);
+                oldVersion++;
+                createEventsView = true;
+            }
             if (createEventsView) {
                 createEventsView(db);
             }
@@ -1230,6 +1403,33 @@
     }
 
     @VisibleForTesting
+    void upgradeToVersion308(SQLiteDatabase db) {
+        /*
+         * Changes from version 307 to 308:
+         * - add Colors table to db
+         * - add eventColor_index to Events table
+         * - add calendar_color_index to Calendars table
+         * - add allowedAttendeeTypes to Calendars table
+         * - add allowedAvailability to Calendars table
+         */
+        createColorsTable(db);
+
+        db.execSQL("ALTER TABLE Calendars ADD COLUMN allowedAvailability TEXT DEFAULT '0,1';");
+        db.execSQL("ALTER TABLE Calendars ADD COLUMN allowedAttendeeTypes TEXT DEFAULT '0,1,2';");
+        db.execSQL("ALTER TABLE Calendars ADD COLUMN calendar_color_index TEXT;");
+        db.execSQL("ALTER TABLE Events ADD COLUMN eventColor_index TEXT;");
+
+        // Default Exchange calendars to be supporting the 'tentative'
+        // availability as well
+        db.execSQL("UPDATE Calendars SET allowedAvailability='0,1,2' WHERE _id IN "
+                + "(SELECT _id FROM Calendars WHERE account_type='com.android.exchange');");
+
+        // Triggers to update the color stored in an event or a calendar when
+        // the color_index is changed.
+        createColorsTriggers(db);
+    }
+
+    @VisibleForTesting
     void upgradeToVersion307(SQLiteDatabase db) {
         /*
          * Changes from version 306 to 307:
@@ -1239,7 +1439,7 @@
         db.execSQL("DROP TRIGGER IF EXISTS events_cleanup_delete");
         db.execSQL("DROP TRIGGER IF EXISTS original_sync_update");
         db.execSQL("DROP INDEX IF EXISTS eventsCalendarIdIndex");
-        createEventsTable(db);
+        createEventsTable307(db);
 
         String FIELD_LIST =
             "_id, " +
@@ -1350,7 +1550,7 @@
         // rename old table, create new table with updated layout
         db.execSQL("ALTER TABLE Calendars RENAME TO Calendars_Backup;");
         db.execSQL("DROP TRIGGER IF EXISTS calendar_cleanup");
-        createCalendarsTable(db);
+        createCalendarsTable305(db);
 
         // copy fields from old to new
         db.execSQL("INSERT INTO Calendars (" +
@@ -1423,7 +1623,7 @@
         // addition of "autoincrement" to _ID doesn't affect the upgrade path.  (Note that
         // much older databases may also already have autoincrement set because the change
         // was back-ported.)
-        createEventsTable(db);
+        createEventsTable307(db);
 
         // copy fields from old to new
         db.execSQL("INSERT INTO Events (" +
@@ -2718,6 +2918,7 @@
     }
 
     private void dropTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.COLORS + ";");
         db.execSQL("DROP TABLE IF EXISTS " + Tables.CALENDARS + ";");
         db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS + ";");
         db.execSQL("DROP TABLE IF EXISTS " + Tables.EVENTS_RAW_TIMES + ";");
@@ -2771,6 +2972,7 @@
                 + CalendarContract.Events.DESCRIPTION + ","
                 + CalendarContract.Events.EVENT_LOCATION + ","
                 + CalendarContract.Events.EVENT_COLOR + ","
+                + CalendarContract.Events.EVENT_COLOR_KEY + ","
                 + CalendarContract.Events.STATUS + ","
                 + CalendarContract.Events.SELF_ATTENDEE_STATUS + ","
                 + CalendarContract.Events.DTSTART + ","
@@ -2824,9 +3026,12 @@
                 + CalendarContract.Calendars.CALENDAR_LOCATION + ","
                 + CalendarContract.Calendars.VISIBLE + ","
                 + CalendarContract.Calendars.CALENDAR_COLOR + ","
+                + CalendarContract.Calendars.CALENDAR_COLOR_KEY + ","
                 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL + ","
                 + CalendarContract.Calendars.MAX_REMINDERS + ","
                 + CalendarContract.Calendars.ALLOWED_REMINDERS + ","
+                + CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES + ","
+                + CalendarContract.Calendars.ALLOWED_AVAILABILITY + ","
                 + CalendarContract.Calendars.CAN_ORGANIZER_RESPOND + ","
                 + CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE + ","
                 + CalendarContract.Calendars.CAN_PARTIALLY_UPDATE + ","
diff --git a/src/com/android/providers/calendar/CalendarProvider2.java b/src/com/android/providers/calendar/CalendarProvider2.java
index 755f330..47d59f9 100644
--- a/src/com/android/providers/calendar/CalendarProvider2.java
+++ b/src/com/android/providers/calendar/CalendarProvider2.java
@@ -50,6 +50,7 @@
 import android.provider.CalendarContract.Attendees;
 import android.provider.CalendarContract.CalendarAlerts;
 import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Colors;
 import android.provider.CalendarContract.Events;
 import android.provider.CalendarContract.Instances;
 import android.provider.CalendarContract.Reminders;
@@ -109,6 +110,28 @@
     private static final int EVENTS_ORIGINAL_ID_INDEX = 3;
     private static final int EVENTS_ORIGINAL_SYNC_ID_INDEX = 4;
 
+    private static final String[] COLORS_PROJECTION = new String[] {
+        Colors.ACCOUNT_NAME,
+        Colors.ACCOUNT_TYPE,
+        Colors.COLOR_TYPE,
+        Colors.COLOR_KEY,
+        Colors.COLOR,
+    };
+    private static final int COLORS_ACCOUNT_NAME_INDEX = 0;
+    private static final int COLORS_ACCOUNT_TYPE_INDEX = 1;
+    private static final int COLORS_COLOR_TYPE_INDEX = 2;
+    private static final int COLORS_COLOR_INDEX_INDEX = 3;
+    private static final int COLORS_COLOR_INDEX = 4;
+
+    private static final String GENERIC_ACCOUNT_NAME = Calendars.ACCOUNT_NAME;
+    private static final String GENERIC_ACCOUNT_TYPE = Calendars.ACCOUNT_TYPE;
+    private static final String[] ACCOUNT_PROJECTION = new String[] {
+        GENERIC_ACCOUNT_NAME,
+        GENERIC_ACCOUNT_TYPE,
+    };
+    private static final int ACCOUNT_NAME_INDEX = 0;
+    private static final int ACCOUNT_TYPE_INDEX = 1;
+
     // many tables have _id and event_id; pick a representative version to use as our generic
     private static final String GENERIC_ID = Attendees._ID;
     private static final String GENERIC_EVENT_ID = Attendees.EVENT_ID;
@@ -171,6 +194,12 @@
             " SET " + Events.DIRTY + "=1" +
             " WHERE " + Events._ID + "=?";
 
+    private static final String SQL_WHERE_CALENDAR_COLOR = Calendars.ACCOUNT_NAME + "=? AND "
+            + Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.CALENDAR_COLOR_KEY + "=?";
+
+    private static final String SQL_WHERE_EVENT_COLOR = Events.ACCOUNT_NAME + "=? AND "
+            + Events.ACCOUNT_TYPE + "=? AND " + Events.EVENT_COLOR_KEY + "=?";
+
     protected static final String SQL_WHERE_ID = GENERIC_ID + "=?";
     private static final String SQL_WHERE_EVENT_ID = GENERIC_EVENT_ID + "=?";
     private static final String SQL_WHERE_ORIGINAL_ID = Events.ORIGINAL_ID + "=?";
@@ -208,6 +237,9 @@
                 " WHERE " + Calendars.ACCOUNT_NAME + "=? AND " +
                     Calendars.ACCOUNT_TYPE + "=?";
 
+    private static final String SQL_DELETE_FROM_COLORS = "DELETE FROM " + Tables.COLORS + " WHERE "
+            + Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?";
+
     private static final String SQL_SELECT_COUNT_FOR_SYNC_ID =
             "SELECT COUNT(*) FROM " + Tables.EVENTS + " WHERE " + Events._SYNC_ID + "=?";
 
@@ -340,6 +372,8 @@
         ALLOWED_IN_EXCEPTION.add(Events.TITLE);
         ALLOWED_IN_EXCEPTION.add(Events.EVENT_LOCATION);
         ALLOWED_IN_EXCEPTION.add(Events.DESCRIPTION);
+        ALLOWED_IN_EXCEPTION.add(Events.EVENT_COLOR);
+        ALLOWED_IN_EXCEPTION.add(Events.EVENT_COLOR_KEY);
         ALLOWED_IN_EXCEPTION.add(Events.STATUS);
         ALLOWED_IN_EXCEPTION.add(Events.SELF_ATTENDEE_STATUS);
         ALLOWED_IN_EXCEPTION.add(Events.SYNC_DATA6);
@@ -809,6 +843,12 @@
                 qb.appendWhere(SQL_WHERE_ID);
                 break;
 
+            case COLORS:
+                qb.setTables(Tables.COLORS);
+                qb.setProjectionMap(sColorsProjectionMap);
+                selection = appendAccountFromParameterToSelection(selection, uri);
+                break;
+
             case CALENDARS:
             case CALENDAR_ENTITIES:
                 qb.setTables(Tables.CALENDARS);
@@ -1655,6 +1695,23 @@
             }
             //DatabaseUtils.dumpCursor(cursor);
 
+            // If there's a color index check that it's valid
+            String color_index = modValues.getAsString(Events.EVENT_COLOR_KEY);
+            if (!TextUtils.isEmpty(color_index)) {
+                int calIdCol = cursor.getColumnIndex(Events.CALENDAR_ID);
+                Long calId = cursor.getLong(calIdCol);
+                String accountName = null;
+                String accountType = null;
+                if (calId != null) {
+                    Account account = getAccount(calId);
+                    if (account != null) {
+                        accountName = account.name;
+                        accountType = account.type;
+                    }
+                }
+                verifyColorExists(accountName, accountType, color_index, Colors.TYPE_EVENT);
+            }
+
             /*
              * Verify that the original event is in fact a recurring event by checking for the
              * presence of an RRULE.  If it's there, we assume that the event is otherwise
@@ -1687,6 +1744,8 @@
             // and drop in the new caller-supplied values.  This will set originalInstanceTime.
             ContentValues values = new ContentValues();
             DatabaseUtils.cursorRowToContentValues(cursor, values);
+            cursor.close();
+            cursor = null;
 
             // TODO: if we're changing this to an all-day event, we should ensure that
             //       hours/mins/secs on DTSTART are zeroed out (before computing DTEND).
@@ -1884,13 +1943,9 @@
                      * the Calendar.  We're expecting to find one matching entry in Attendees.
                      */
                     long calendarId = values.getAsLong(Events.CALENDAR_ID);
-                    cursor = mDb.query(Tables.CALENDARS, new String[] { Calendars.OWNER_ACCOUNT },
-                            SQL_WHERE_ID, new String[] { String.valueOf(calendarId) },
-                            null /* groupBy */, null /* having */, null /* sortOrder */);
-                    if (!cursor.moveToFirst()) {
-                        Log.w(TAG, "Can't get calendar account_name for calendar " + calendarId);
-                    } else {
-                        String accountName = cursor.getString(0);
+                    String accountName = getOwner(calendarId);
+
+                    if (accountName != null) {
                         ContentValues attValues = new ContentValues();
                         attValues.put(Attendees.ATTENDEE_STATUS,
                                 modValues.getAsString(Events.SELF_ATTENDEE_STATUS));
@@ -1906,8 +1961,8 @@
                         if (count != 1 && count != 2) {
                             // We're only expecting one matching entry.  We might briefly see
                             // two during a server sync.
-                            Log.e(TAG, "Attendee status update on event=" + newEventId +
-                                    " name=" + accountName + " touched " + count + " rows");
+                            Log.e(TAG, "Attendee status update on event=" + newEventId
+                                    + " touched " + count + " rows. Expected one or two rows.");
                             if (false) {
                                 // This dumps PII in the log, don't ship with it enabled.
                                 Cursor debugCursor = mDb.query(Tables.ATTENDEES, null,
@@ -1920,7 +1975,6 @@
                             throw new RuntimeException("Status update WTF");
                         }
                     }
-                    cursor.close();
                 }
             } else {
                 /*
@@ -2010,10 +2064,29 @@
                     throw new RuntimeException("Could not insert event.");
                     // return null;
                 }
+                Long calendar_id = updatedValues.getAsLong(Events.CALENDAR_ID);
+                if (calendar_id == null) {
+                    // validateEventData checks this for non-sync adapter
+                    // inserts
+                    throw new IllegalArgumentException("New events must specify a calendar id");
+                }
+                // Verify the color is valid if it is being set
+                String color_id = updatedValues.getAsString(Events.EVENT_COLOR_KEY);
+                if (!TextUtils.isEmpty(color_id)) {
+                    Account account = getAccount(calendar_id);
+                    String accountName = null;
+                    String accountType = null;
+                    if (account != null) {
+                        accountName = account.name;
+                        accountType = account.type;
+                    }
+                    int color = verifyColorExists(accountName, accountType, color_id,
+                            Colors.TYPE_EVENT);
+                    updatedValues.put(Events.EVENT_COLOR, color);
+                }
                 String owner = null;
-                if (updatedValues.containsKey(Events.CALENDAR_ID) &&
-                        !updatedValues.containsKey(Events.ORGANIZER)) {
-                    owner = getOwner(updatedValues.getAsLong(Events.CALENDAR_ID));
+                if (!updatedValues.containsKey(Events.ORGANIZER)) {
+                    owner = getOwner(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,
@@ -2056,43 +2129,10 @@
                     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));
+                            owner = getOwner(calendar_id);
                         }
                         createAttendeeEntry(id, status, owner);
                     }
-                    // if the Event Timezone is defined, store it as the original one in the
-                    // ExtendedProperties table
-                    if (values.containsKey(Events.EVENT_TIMEZONE) && !callerIsSyncAdapter) {
-                        String originalTimezone = values.getAsString(Events.EVENT_TIMEZONE);
-
-                        ContentValues expropsValues = new ContentValues();
-                        expropsValues.put(CalendarContract.ExtendedProperties.EVENT_ID, id);
-                        expropsValues.put(CalendarContract.ExtendedProperties.NAME,
-                                EXT_PROP_ORIGINAL_TIMEZONE);
-                        expropsValues.put(CalendarContract.ExtendedProperties.VALUE,
-                                originalTimezone);
-
-                        // Insert the extended property
-                        long exPropId = mDbHelper.extendedPropertiesInsert(expropsValues);
-                        if (exPropId == -1) {
-                            if (Log.isLoggable(TAG, Log.ERROR)) {
-                                Log.e(TAG, "Cannot add the original Timezone in the "
-                                        + "ExtendedProperties table for Event: " + id);
-                            }
-                        } else {
-                            // Update the Event for saying it has some extended properties
-                            ContentValues eventValues = new ContentValues();
-                            eventValues.put(Events.HAS_EXTENDED_PROPERTIES, "1");
-                            int result = mDb.update("Events", eventValues, SQL_WHERE_ID,
-                                    new String[] {String.valueOf(id)});
-                            if (result <= 0) {
-                                if (Log.isLoggable(TAG, Log.ERROR)) {
-                                    Log.e(TAG, "Cannot update hasExtendedProperties column"
-                                            + " for Event: " + id);
-                                }
-                            }
-                        }
-                    }
 
                     backfillExceptionOriginalIds(id, values);
 
@@ -2114,9 +2154,56 @@
                     String eventsUrl = values.getAsString(Calendars.CAL_SYNC1);
                     mDbHelper.scheduleSync(account, false /* two-way sync */, eventsUrl);
                 }
+                String cal_color_id = values.getAsString(Calendars.CALENDAR_COLOR_KEY);
+                if (!TextUtils.isEmpty(cal_color_id)) {
+                    String accountName = values.getAsString(Calendars.ACCOUNT_NAME);
+                    String accountType = values.getAsString(Calendars.ACCOUNT_TYPE);
+                    int color = verifyColorExists(accountName, accountType, cal_color_id,
+                            Colors.TYPE_CALENDAR);
+                    values.put(Calendars.CALENDAR_COLOR, color);
+                }
                 id = mDbHelper.calendarsInsert(values);
                 sendUpdateNotification(id, callerIsSyncAdapter);
                 break;
+            case COLORS:
+                // verifyTransactionAllowed requires this be from a sync
+                // adapter, all of the required fields are marked NOT NULL in
+                // the db. TODO Do we need explicit checks here or should we
+                // just let sqlite throw if something isn't specified?
+                String accountName = uri.getQueryParameter(Colors.ACCOUNT_NAME);
+                String accountType = uri.getQueryParameter(Colors.ACCOUNT_TYPE);
+                String colorIndex = values.getAsString(Colors.COLOR_KEY);
+                if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+                    throw new IllegalArgumentException("Account name and type must be non"
+                            + " empty parameters for " + uri);
+                }
+                if (TextUtils.isEmpty(colorIndex)) {
+                    throw new IllegalArgumentException("COLOR_INDEX must be non empty for " + uri);
+                }
+                if (!values.containsKey(Colors.COLOR_TYPE) || !values.containsKey(Colors.COLOR)) {
+                    throw new IllegalArgumentException(
+                            "New colors must contain COLOR_TYPE and COLOR");
+                }
+                // Make sure the account we're inserting for is the same one the
+                // adapter is claiming to be. TODO should we throw if they
+                // aren't the same?
+                values.put(Colors.ACCOUNT_NAME, accountName);
+                values.put(Colors.ACCOUNT_TYPE, accountType);
+
+                // Verify the color doesn't already exist
+                Cursor c = null;
+                try {
+                    c = getColorByIndex(accountName, accountType, colorIndex);
+                    if (c.getCount() != 0) {
+                        throw new IllegalArgumentException(colorIndex
+                                + " already exists for account and type provided");
+                    }
+                } finally {
+                    if (c != null)
+                        c.close();
+                }
+                id = mDbHelper.colorsInsert(values);
+                break;
             case ATTENDEES:
                 if (!values.containsKey(Attendees.EVENT_ID)) {
                     throw new IllegalArgumentException("Attendees values must "
@@ -2435,9 +2522,16 @@
         return originalSyncId;
     }
 
+    private Cursor getColorByIndex(String accountName, String accountType, String index) {
+        return mDb.query(Tables.COLORS, COLORS_PROJECTION, Colors.ACCOUNT_NAME + "=? AND "
+                + Colors.ACCOUNT_TYPE + "=? AND " + Colors.COLOR_KEY + "=?",
+                new String[] { accountName, accountType, index }, null, null, null);
+    }
+
     /**
-     * Gets the calendar's owner for an event.
-     * @param calId
+     * Gets a calendar's "owner account", i.e. the e-mail address of the owner of the calendar.
+     *
+     * @param calId The calendar ID.
      * @return email of owner or null
      */
     private String getOwner(long calId) {
@@ -2471,6 +2565,29 @@
         return emailAddress;
     }
 
+    private Account getAccount(long calId) {
+        Account account = null;
+        Cursor cursor = null;
+        try {
+            cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, calId),
+                    ACCOUNT_PROJECTION, null /* selection */, null /* selectionArgs */,
+                    null /* sort */);
+            if (cursor == null || !cursor.moveToFirst()) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Couldn't find " + calId + " in Calendars table");
+                }
+                return null;
+            }
+            account = new Account(cursor.getString(ACCOUNT_NAME_INDEX),
+                    cursor.getString(ACCOUNT_TYPE_INDEX));
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return account;
+    }
+
     /**
      * Creates an entry in the Attendees table that refers to the given event
      * and that has the given response status.
@@ -2812,6 +2929,10 @@
                 return mDbHelper.getSyncState().delete(mDb, selectionWithId,
                         selectionArgs);
 
+            case COLORS:
+                return deleteMatchingColors(appendAccountToSelection(uri, selection),
+                        selectionArgs);
+
             case EVENTS:
             {
                 int result = 0;
@@ -3294,6 +3415,56 @@
         return count;
     }
 
+    private int deleteMatchingColors(String selection, String[] selectionArgs) {
+        // query to find all the colors that match, for each
+        // - verify no one references it
+        // - delete color
+        Cursor c = mDb.query(Tables.COLORS, COLORS_PROJECTION, selection, selectionArgs, null,
+                null, null);
+        if (c == null) {
+            return 0;
+        }
+        try {
+            Cursor c2 = null;
+            while (c.moveToNext()) {
+                String index = c.getString(COLORS_COLOR_INDEX_INDEX);
+                String accountName = c.getString(COLORS_ACCOUNT_NAME_INDEX);
+                String accountType = c.getString(COLORS_ACCOUNT_TYPE_INDEX);
+                boolean isCalendarColor = c.getInt(COLORS_COLOR_TYPE_INDEX) == Colors.TYPE_CALENDAR;
+                try {
+                    if (isCalendarColor) {
+                        c2 = mDb.query(Tables.CALENDARS, ID_ONLY_PROJECTION,
+                                SQL_WHERE_CALENDAR_COLOR, new String[] {
+                                        accountName, accountType, index
+                                }, null, null, null);
+                        if (c2.getCount() != 0) {
+                            throw new UnsupportedOperationException("Cannot delete color " + index
+                                    + ". Referenced by " + c2.getCount() + " calendars.");
+
+                        }
+                    } else {
+                        c2 = query(Events.CONTENT_URI, ID_ONLY_PROJECTION, SQL_WHERE_EVENT_COLOR,
+                                new String[] {accountName, accountType, index}, null);
+                        if (c2.getCount() != 0) {
+                            throw new UnsupportedOperationException("Cannot delete color " + index
+                                    + ". Referenced by " + c2.getCount() + " events.");
+
+                        }
+                    }
+                } finally {
+                    if (c2 != null) {
+                        c2.close();
+                    }
+                }
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return mDb.delete(Tables.COLORS, selection, selectionArgs);
+    }
+
     private int deleteMatchingCalendars(String selection, String[] selectionArgs) {
         // query to find all the calendars that match, for each
         // - delete calendar subscription
@@ -3355,6 +3526,40 @@
         return true;
     }
 
+    private int handleUpdateColors(ContentValues values, String selection, String[] selectionArgs) {
+        Cursor c = null;
+        int result = mDb.update(Tables.COLORS, values, selection, selectionArgs);
+        if (values.containsKey(Colors.COLOR)) {
+            try {
+                c = mDb.query(Tables.COLORS, COLORS_PROJECTION, selection, selectionArgs,
+                        null /* groupBy */, null /* having */, null /* orderBy */);
+                while (c.moveToNext()) {
+                    boolean calendarColor =
+                            c.getInt(COLORS_COLOR_TYPE_INDEX) == Colors.TYPE_CALENDAR;
+                    int color = c.getInt(COLORS_COLOR_INDEX);
+                    String[] args = {
+                            c.getString(COLORS_ACCOUNT_NAME_INDEX),
+                            c.getString(COLORS_ACCOUNT_TYPE_INDEX),
+                            c.getString(COLORS_COLOR_INDEX_INDEX)
+                    };
+                    ContentValues colorValue = new ContentValues();
+                    if (calendarColor) {
+                        colorValue.put(Calendars.CALENDAR_COLOR, color);
+                        mDb.update(Tables.CALENDARS, values, SQL_WHERE_CALENDAR_COLOR, args);
+                    } else {
+                        colorValue.put(Events.EVENT_COLOR, color);
+                        mDb.update(Tables.EVENTS, values, SQL_WHERE_EVENT_COLOR, args);
+                    }
+                }
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+        }
+        return result;
+    }
+
 
     /**
      * Handles a request to update one or more events.
@@ -3419,6 +3624,26 @@
             // Merge the modifications in.
             values.putAll(modValues);
 
+            // If a color_index is being set make sure it's valid
+            String color_id = modValues.getAsString(Events.EVENT_COLOR_KEY);
+            if (!TextUtils.isEmpty(color_id)) {
+                String accountName = null;
+                String accountType = null;
+                Cursor c = mDb.query(Tables.CALENDARS, ACCOUNT_PROJECTION, SQL_WHERE_ID,
+                        new String[] { values.getAsString(Events.CALENDAR_ID) }, null, null, null);
+                try {
+                    if (c.moveToFirst()) {
+                        accountName = c.getString(ACCOUNT_NAME_INDEX);
+                        accountType = c.getString(ACCOUNT_TYPE_INDEX);
+                    }
+                } finally {
+                    if (c != null) {
+                        c.close();
+                    }
+                }
+                verifyColorExists(accountName, accountType, color_id, Colors.TYPE_EVENT);
+            }
+
             // Scrub and/or validate the combined event.
             if (callerIsSyncAdapter) {
                 scrubEventData(values, modValues);
@@ -3559,6 +3784,15 @@
                 return mDbHelper.getSyncState().update(mDb, values, selectionWithId, selectionArgs);
             }
 
+            case COLORS:
+                Integer color = values.getAsInteger(Colors.COLOR);
+                if (values.size() > 1 || (values.size() == 1 && color == null)) {
+                    throw new UnsupportedOperationException("You may only change the COLOR "
+                            + "for an existing Colors entry.");
+                }
+                return handleUpdateColors(values, appendAccountToSelection(uri, selection),
+                        selectionArgs);
+
             case CALENDARS:
             case CALENDARS_ID:
             {
@@ -3588,6 +3822,19 @@
                 if (syncEvents != null) {
                     modifyCalendarSubscription(id, syncEvents == 1);
                 }
+                String color_id = values.getAsString(Calendars.CALENDAR_COLOR_KEY);
+                if (!TextUtils.isEmpty(color_id)) {
+                    String accountName = values.getAsString(Calendars.ACCOUNT_NAME);
+                    String accountType = values.getAsString(Calendars.ACCOUNT_TYPE);
+                    if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+                        Account account = getAccount(id);
+                        if (account != null) {
+                            accountName = account.name;
+                            accountType = account.type;
+                        }
+                    }
+                    verifyColorExists(accountName, accountType, color_id, Colors.TYPE_CALENDAR);
+                }
 
                 int result = mDb.update(Tables.CALENDARS, values, SQL_WHERE_ID,
                         new String[] {String.valueOf(id)});
@@ -3777,6 +4024,39 @@
         }
     }
 
+    /**
+     * Verifies that a color with the given index exists for the given Calendar
+     * entry.
+     *
+     * @param accountName The email of the account the color is for
+     * @param accountType The type of account the color is for
+     * @param color_index The color_index being set for the calendar
+     * @param color_type The type of color expected (Calendar/Event)
+     * @return The color specified by the index
+     */
+    private int verifyColorExists(String accountName, String accountType, String color_index,
+            int color_type) {
+        if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) {
+            throw new IllegalArgumentException("Cannot set color. A valid account does"
+                    + " not exist for this calendar.");
+        }
+        int color;
+        Cursor c = null;
+        try {
+            c = getColorByIndex(accountName, accountType, color_index);
+            if (!c.moveToFirst() || c.getInt(COLORS_COLOR_TYPE_INDEX) != color_type) {
+                throw new IllegalArgumentException(color_index
+                        + " color does not exist for account or is the wrong type.");
+            }
+            color = c.getInt(COLORS_COLOR_INDEX);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return color;
+    }
+
     private String appendAccountFromParameterToSelection(String selection, Uri uri) {
         final String accountName = QueryParameterUtils.getQueryParameter(uri,
                 CalendarContract.EventsEntity.ACCOUNT_NAME);
@@ -3869,8 +4149,10 @@
         }
 
         if (type == TRANSACTION_UPDATE || type == TRANSACTION_DELETE) {
+            // TODO review this list, document in contract.
             if (!TextUtils.isEmpty(selection)) {
                 // Only allow selections for the URIs that can reasonably use them.
+                // Whitelist of URIs allowed selections
                 switch (uriMatch) {
                     case SYNCSTATE:
                     case CALENDARS:
@@ -3880,12 +4162,14 @@
                     case REMINDERS:
                     case EXTENDED_PROPERTIES:
                     case PROVIDER_PROPERTIES:
+                    case COLORS:
                         break;
                     default:
                         throw new IllegalArgumentException("Selection not permitted for " + uri);
                 }
             } else {
                 // Disallow empty selections for some URIs.
+                // Blacklist of URIs _not_ allowed empty selections
                 switch (uriMatch) {
                     case EVENTS:
                     case ATTENDEES:
@@ -3900,9 +4184,16 @@
         }
 
         // Only the sync adapter can use these to make changes.
-        if (uriMatch == SYNCSTATE || uriMatch == EXTENDED_PROPERTIES) {
-            if (!isSyncAdapter) {
-                throw new IllegalArgumentException("Only sync adapters may use " + uri);
+        if (!isSyncAdapter) {
+            switch (uriMatch) {
+                case SYNCSTATE:
+                case SYNCSTATE_ID:
+                case EXTENDED_PROPERTIES:
+                case EXTENDED_PROPERTIES_ID:
+                case COLORS:
+                    throw new IllegalArgumentException("Only sync adapters may write using " + uri);
+                default:
+                    break;
             }
         }
 
@@ -4042,7 +4333,8 @@
                     oldSyncEvents = (cursor.getInt(3) != 0);
                 }
             } finally {
-                cursor.close();
+                if (cursor != null)
+                    cursor.close();
             }
         }
 
@@ -4184,9 +4476,11 @@
     private static final int EXCEPTION_ID = 29;
     private static final int EXCEPTION_ID2 = 30;
     private static final int EMMA = 31;
+    private static final int COLORS = 32;
 
     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     private static final HashMap<String, String> sInstancesProjectionMap;
+    private static final HashMap<String, String> sColorsProjectionMap;
     protected static final HashMap<String, String> sEventsProjectionMap;
     private static final HashMap<String, String> sEventEntitiesProjectionMap;
     private static final HashMap<String, String> sAttendeesProjectionMap;
@@ -4233,11 +4527,21 @@
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#", EXCEPTION_ID);
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "exception/#/#", EXCEPTION_ID2);
         sUriMatcher.addURI(CalendarContract.AUTHORITY, "emma", EMMA);
+        sUriMatcher.addURI(CalendarContract.AUTHORITY, "colors", COLORS);
 
         /** Contains just BaseColumns._COUNT */
         sCountProjectionMap = new HashMap<String, String>();
         sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
 
+        sColorsProjectionMap = new HashMap<String, String>();
+        sColorsProjectionMap.put(Colors._ID, Colors._ID);
+        sColorsProjectionMap.put(Colors.DATA, Colors.DATA);
+        sColorsProjectionMap.put(Colors.ACCOUNT_NAME, Colors.ACCOUNT_NAME);
+        sColorsProjectionMap.put(Colors.ACCOUNT_TYPE, Colors.ACCOUNT_TYPE);
+        sColorsProjectionMap.put(Colors.COLOR_KEY, Colors.COLOR_KEY);
+        sColorsProjectionMap.put(Colors.COLOR_TYPE, Colors.COLOR_TYPE);
+        sColorsProjectionMap.put(Colors.COLOR, Colors.COLOR);
+
         sEventsProjectionMap = new HashMap<String, String>();
         // Events columns
         sEventsProjectionMap.put(Events.ACCOUNT_NAME, Events.ACCOUNT_NAME);
@@ -4247,6 +4551,7 @@
         sEventsProjectionMap.put(Events.DESCRIPTION, Events.DESCRIPTION);
         sEventsProjectionMap.put(Events.STATUS, Events.STATUS);
         sEventsProjectionMap.put(Events.EVENT_COLOR, Events.EVENT_COLOR);
+        sEventsProjectionMap.put(Events.EVENT_COLOR_KEY, Events.EVENT_COLOR_KEY);
         sEventsProjectionMap.put(Events.SELF_ATTENDEE_STATUS, Events.SELF_ATTENDEE_STATUS);
         sEventsProjectionMap.put(Events.DTSTART, Events.DTSTART);
         sEventsProjectionMap.put(Events.DTEND, Events.DTEND);
@@ -4282,12 +4587,16 @@
 
         // Calendar columns
         sEventsProjectionMap.put(Calendars.CALENDAR_COLOR, Calendars.CALENDAR_COLOR);
+        sEventsProjectionMap.put(Calendars.CALENDAR_COLOR_KEY, Calendars.CALENDAR_COLOR_KEY);
         sEventsProjectionMap.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CALENDAR_ACCESS_LEVEL);
         sEventsProjectionMap.put(Calendars.VISIBLE, Calendars.VISIBLE);
         sEventsProjectionMap.put(Calendars.CALENDAR_TIME_ZONE, Calendars.CALENDAR_TIME_ZONE);
         sEventsProjectionMap.put(Calendars.OWNER_ACCOUNT, Calendars.OWNER_ACCOUNT);
         sEventsProjectionMap.put(Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_DISPLAY_NAME);
         sEventsProjectionMap.put(Calendars.ALLOWED_REMINDERS, Calendars.ALLOWED_REMINDERS);
+        sEventsProjectionMap
+                .put(Calendars.ALLOWED_ATTENDEE_TYPES, Calendars.ALLOWED_ATTENDEE_TYPES);
+        sEventsProjectionMap.put(Calendars.ALLOWED_AVAILABILITY, Calendars.ALLOWED_AVAILABILITY);
         sEventsProjectionMap.put(Calendars.MAX_REMINDERS, Calendars.MAX_REMINDERS);
         sEventsProjectionMap.put(Calendars.CAN_ORGANIZER_RESPOND, Calendars.CAN_ORGANIZER_RESPOND);
         sEventsProjectionMap.put(Calendars.CAN_MODIFY_TIME_ZONE, Calendars.CAN_MODIFY_TIME_ZONE);
@@ -4475,21 +4784,20 @@
             return;
         }
 
-        HashMap<Account, Boolean> accountHasCalendar = new HashMap<Account, Boolean>();
         HashSet<Account> validAccounts = new HashSet<Account>();
         for (Account account : accounts) {
             validAccounts.add(new Account(account.name, account.type));
-            accountHasCalendar.put(account, false);
         }
         ArrayList<Account> accountsToDelete = new ArrayList<Account>();
 
         mDb.beginTransaction();
+        Cursor c = null;
         try {
 
-            for (String table : new String[]{Tables.CALENDARS}) {
+            for (String table : new String[]{Tables.CALENDARS, Tables.COLORS}) {
                 // Find all the accounts the calendar DB knows about, mark the ones that aren't
                 // in the valid set for deletion.
-                Cursor c = mDb.rawQuery("SELECT DISTINCT " +
+                c = mDb.rawQuery("SELECT DISTINCT " +
                                             Calendars.ACCOUNT_NAME +
                                             "," +
                                             Calendars.ACCOUNT_TYPE +
@@ -4510,6 +4818,7 @@
                     }
                 }
                 c.close();
+                c = null;
             }
 
             for (Account account : accountsToDelete) {
@@ -4518,10 +4827,15 @@
                 }
                 String[] params = new String[]{account.name, account.type};
                 mDb.execSQL(SQL_DELETE_FROM_CALENDARS, params);
+                // This will be a no-op for accounts without a color palette.
+                mDb.execSQL(SQL_DELETE_FROM_COLORS, params);
             }
             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
             mDb.setTransactionSuccessful();
         } finally {
+            if (c != null) {
+                c.close();
+            }
             mDb.endTransaction();
         }
 
diff --git a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
index 608984b..bbd98ff 100644
--- a/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
+++ b/tests/src/com/android/providers/calendar/CalendarProvider2Test.java
@@ -16,8 +16,6 @@
 
 package com.android.providers.calendar;
 
-import com.android.common.ArrayListCursor;
-
 import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -29,6 +27,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.database.MatrixCursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
@@ -2424,7 +2423,7 @@
         @Override
         public Cursor query(Uri uri, String[] projection, String selection,
                 String[] selectionArgs, String sortOrder) {
-            return new ArrayListCursor(new String[]{}, new ArrayList<ArrayList>());
+            return new MatrixCursor(new String[]{ "_id" }, 0);
         }
 
         @Override