Initial implementation of guest invitation and status

Added a number of TODO's which will be taken care of later. Performance
needs to be improved.  Also changed some of the content provider calls
to the batch API.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4fcd945..8805d4c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,6 +24,7 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
     <uses-permission android:name="android.permission.READ_CALENDAR" />
     <uses-permission android:name="android.permission.WRITE_CALENDAR" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -58,7 +59,7 @@
             android:exported="true" />
         
         <activity android:name="EditEvent" android:label="@string/event_edit_title"
-            android:theme="@android:style/Theme.Light"
+            android:theme="@android:style/Theme"
             android:configChanges="orientation|keyboardHidden">
             
             <intent-filter>
diff --git a/res/drawable/im_avatar_picture_border_normal.9.png b/res/drawable/im_avatar_picture_border_normal.9.png
new file mode 100644
index 0000000..01cc9dc
--- /dev/null
+++ b/res/drawable/im_avatar_picture_border_normal.9.png
Binary files differ
diff --git a/res/layout/contact_item.xml b/res/layout/contact_item.xml
new file mode 100644
index 0000000..1d37f52
--- /dev/null
+++ b/res/layout/contact_item.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:paddingLeft="2dip"
+    android:paddingRight="10dip"
+    android:minHeight="46dip">
+
+    <ImageView
+        android:id="@+id/avatar"
+        android:scaleType="centerCrop"
+        android:paddingLeft="4dip"
+        android:layout_width="46dip"
+        android:layout_height="46dip"
+        android:layout_marginLeft="10dip"
+        android:layout_marginTop="10dip"
+        android:layout_marginBottom="10dip"
+        android:layout_alignParentLeft="true"
+        android:layout_centerVertical="true"
+        android:background="@drawable/im_avatar_picture_border_normal" />
+
+    <TextView
+        android:id="@+id/name"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:inputType="none"
+        android:paddingLeft="5dip"
+        android:layout_centerVertical="true"
+        android:layout_toRightOf="@id/avatar"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+
+    <ImageView
+        android:id="@+id/presence"
+        android:scaleType="fitXY"
+        android:visibility="gone"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <View
+        android:id="@+id/separator"
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:layout_alignParentBottom="true"
+        android:background="@android:drawable/divider_horizontal_bright" />
+
+</RelativeLayout>
diff --git a/res/layout/edit_event.xml b/res/layout/edit_event.xml
index 352a4ed..2dd4a4f 100644
--- a/res/layout/edit_event.xml
+++ b/res/layout/edit_event.xml
@@ -68,14 +68,14 @@
                     android:layout_height="wrap_content"
                     android:layout_weight="7"
                     android:gravity="left|center_vertical"
-                    style="?android:attr/textAppearanceMedium"/>
+                    style="?android:attr/textAppearanceMediumInverse"/>
 
                 <Button android:id="@+id/start_time"
                     android:layout_width="0px"
                     android:layout_height="wrap_content"
                     android:layout_weight="4"
                     android:gravity="left|center_vertical"
-                    style="?android:attr/textAppearanceMedium"/>
+                    style="?android:attr/textAppearanceMediumInverse"/>
 
             </LinearLayout>
 
@@ -95,14 +95,14 @@
                     android:layout_height="wrap_content"
                     android:layout_weight="7"
                     android:gravity="left|center_vertical"
-                    style="?android:attr/textAppearanceMedium"/>
+                    style="?android:attr/textAppearanceMediumInverse"/>
 
                 <Button android:id="@+id/end_time"
                     android:layout_width="0px"
                     android:layout_height="wrap_content"
                     android:layout_weight="4"
                     android:gravity="left|center_vertical"
-                    style="?android:attr/textAppearanceMedium"/>
+                    style="?android:attr/textAppearanceMediumInverse"/>
             </LinearLayout>
 
             <LinearLayout
@@ -116,7 +116,7 @@
                     android:text="@string/edit_event_all_day_label"
                     android:paddingTop="1dip"
                     android:paddingRight="7dip"
-                    style="?android:attr/textAppearanceMedium"/>
+                    style="?android:attr/textAppearanceMediumInverse"/>
                 <CheckBox android:id="@+id/is_all_day"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
@@ -195,6 +195,29 @@
                 android:layout_height="wrap_content" />
         </LinearLayout>
 
+        <!-- GUESTS/ATTENDEES -->
+        <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            style="@style/EditEvent_Layout">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/attendees_label"
+                style="@style/TextAppearance.EditEvent_Label"/>
+
+            <MultiAutoCompleteTextView android:id="@+id/attendees"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:inputType="textEmailAddress|textMultiLine"
+                android:hint="@string/hint_attendees"
+                android:layout_marginTop="6dip"
+                android:layout_marginBottom="6dip"
+                android:imeOptions="actionNext"/>
+        </LinearLayout>
+
         <!-- REPEATS -->
         <LinearLayout
             android:orientation="vertical"
@@ -294,7 +317,8 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginRight="2dip"
-                android:text="@string/add_new_reminder"/>
+                android:text="@string/add_new_reminder"
+                style="?android:attr/textAppearanceSmallInverse"/>
 
             <ImageButton android:id="@+id/reminder_add"
                 style="@style/PlusButton"
diff --git a/res/layout/event_info_activity.xml b/res/layout/event_info_activity.xml
index 3992e80..a6b4d73 100644
--- a/res/layout/event_info_activity.xml
+++ b/res/layout/event_info_activity.xml
@@ -154,7 +154,15 @@
                 </LinearLayout>
             </LinearLayout>
         </LinearLayout>
-        
+
+        <!-- GUEST LIST -->
+        <LinearLayout
+            android:id="@+id/attendee_list"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="8dip"
+            android:orientation="vertical" />
+
         <!-- RESPONSE -->
         <LinearLayout android:id="@+id/response_container"
             android:orientation="vertical"
@@ -169,7 +177,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/view_event_response_label"
-                style="@style/TextAppearance.EditEvent_Label"/>
+                style="@style/TextAppearance.EventInfo_Label"/>
         
             <Spinner android:id="@+id/response_value"
                 style="?android:attr/textAppearanceMedium"
@@ -193,7 +201,7 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/reminders_label"
-                style="@style/TextAppearance.EditEvent_Label"/>
+                style="@style/TextAppearance.EventInfo_Label"/>
 
             <LinearLayout android:id="@+id/reminder_items_container"
                 style="?android:attr/textAppearanceMedium"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c463ef6..4286874 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -30,6 +30,11 @@
     <string name="when_label">When</string>
     <!-- This is the label for the location of an event -->
     <string name="where_label">Where</string>
+    <!-- This is the label for the Guests/Attendees of an event -->
+    <string name="attendees_label">Guests</string>
+    <!-- This is the label for the Guest Responses and count of an event e.g. Yes (3) -->
+    <string name="response_label">"<xliff:g id="response_type">%s</xliff:g> (<xliff:g id="guest_count">%d</xliff:g>)"</string>
+    
     <!-- Some events repeat daily, weekly, monthly, or yearly.  This is the label
          for all the choices about how often an event repeats (including the choice
          of not repeating). -->
@@ -151,6 +156,8 @@
     <string name="hint_where">"Event location"</string>
     <!-- Default value of Description field (as a hint to the user) -->
     <string name="hint_description">"Event description"</string>
+    <!-- Default value of Attendees/Guests field (as a hint to the user) -->
+    <string name="hint_attendees">"Email addresses"</string>
     <string name="creating_event">"Creating event\u2026"</string>
     <string name="saving_event">"Saving event\u2026"</string>
     <string name="loading_calendars_title">"Loading calendars"</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4471ad2..0f6e89d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -59,6 +59,13 @@
     
     <style name="TextAppearance.EditEvent_Label">
         <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+        <item name="android:textColor">?android:attr/textColorSecondaryInverse</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:paddingLeft">2dip</item>
+    </style>
+    
+    <style name="TextAppearance.EventInfo_Label">
+        <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
         <item name="android:textStyle">bold</item>
         <item name="android:paddingLeft">2dip</item>
diff --git a/src/com/android/calendar/EditEvent.java b/src/com/android/calendar/EditEvent.java
index a772bb7..1451218 100644
--- a/src/com/android/calendar/EditEvent.java
+++ b/src/com/android/calendar/EditEvent.java
@@ -18,6 +18,7 @@
 
 import static android.provider.Calendar.EVENT_BEGIN_TIME;
 import static android.provider.Calendar.EVENT_END_TIME;
+
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.DatePickerDialog;
@@ -26,28 +27,40 @@
 import android.app.DatePickerDialog.OnDateSetListener;
 import android.app.TimePickerDialog.OnTimeSetListener;
 import android.content.AsyncQueryHandler;
+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.DialogInterface;
 import android.content.Intent;
+import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
+import android.content.ContentProviderOperation.Builder;
 import android.content.DialogInterface.OnCancelListener;
 import android.content.DialogInterface.OnClickListener;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.pim.EventRecurrence;
 import android.preference.PreferenceManager;
+import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
 import android.provider.Calendar.Reminders;
+import android.text.InputFilter;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.text.format.Time;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.text.util.Rfc822Validator;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -62,6 +75,7 @@
 import android.widget.DatePicker;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
+import android.widget.MultiAutoCompleteTextView;
 import android.widget.ResourceCursorAdapter;
 import android.widget.Spinner;
 import android.widget.TextView;
@@ -75,6 +89,8 @@
 
 public class EditEvent extends Activity implements View.OnClickListener,
         DialogInterface.OnCancelListener, DialogInterface.OnClickListener {
+    private static final boolean DEBUG = false;
+
     /**
      * This is the symbolic name for the key used to pass in the boolean
      * for creating all-day events that is part of the extra data of the intent.
@@ -156,7 +172,7 @@
     private static final int MODIFY_SELECTED = 1;
     private static final int MODIFY_ALL = 2;
     private static final int MODIFY_ALL_FOLLOWING = 3;
-    
+
     private static final int DAY_IN_SECONDS = 24 * 60 * 60;
 
     private int mFirstDayOfWeek; // cached in onCreate
@@ -184,6 +200,9 @@
     private LinearLayout mExtraOptions;
     private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
     private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
+    MultiAutoCompleteTextView mAttendeesList;
+    private EmailAddressAdapter mAddressAdapter;
+    private Rfc822Validator mValidator;
 
     private EventRecurrence mEventRecurrence = new EventRecurrence();
     private String mRrule;
@@ -214,7 +233,7 @@
 
     private DeleteEventHelper mDeleteEventHelper;
     private QueryHandler mQueryHandler;
-    
+
     /* This class is used to update the time buttons. */
     private class TimeListener implements OnTimeSetListener {
         private View mView;
@@ -373,7 +392,7 @@
             }
             return;
         }
-        
+
         if (v == mDeleteButton) {
             long begin = mStartTime.toMillis(false /* use isDst */);
             long end = mEndTime.toMillis(false /* use isDst */);
@@ -392,12 +411,12 @@
             mDeleteEventHelper.delete(begin, end, mEventCursor, which);
             return;
         }
-        
+
         if (v == mDiscardButton) {
             finish();
             return;
         }
-        
+
         // This must be a click on one of the "remove reminder" buttons
         LinearLayout reminderItem = (LinearLayout) v.getParent();
         LinearLayout parent = (LinearLayout) reminderItem.getParent();
@@ -426,7 +445,7 @@
             finish();
         }
     }
-    
+
     private class QueryHandler extends AsyncQueryHandler {
         public QueryHandler(ContentResolver cr) {
             super(cr);
@@ -442,7 +461,7 @@
             } else {
                 mCalendarsCursor = cursor;
                 startManagingCursor(cursor);
-                
+
                 // Stop the spinner
                 getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
                         Window.PROGRESS_VISIBILITY_OFF);
@@ -454,7 +473,7 @@
                     if (mSaveAfterQueryComplete) {
                         mLoadingCalendarsDialog.cancel();
                     }
-                    
+
                     // Create an error message for the user that, when clicked,
                     // will exit this activity without saving the event.
                     AlertDialog.Builder builder = new AlertDialog.Builder(EditEvent.this);
@@ -514,7 +533,7 @@
             String rrule = mEventCursor.getString(EVENT_INDEX_RRULE);
             String timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
             long calendarId = mEventCursor.getInt(EVENT_INDEX_CALENDAR_ID);
-            
+
             // Remember the initial values
             mInitialValues = new ContentValues();
             mInitialValues.put(EVENT_BEGIN_TIME, begin);
@@ -527,7 +546,7 @@
             // We are creating a new event, so set the default from the
             // intent (if specified).
             allDay = intent.getBooleanExtra(EVENT_ALL_DAY, false);
-            
+
             // Start the spinner
             getWindow().setFeatureInt(Window.FEATURE_INDETERMINATE_PROGRESS,
                     Window.PROGRESS_VISIBILITY_ON);
@@ -584,6 +603,10 @@
         mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
         mExtraOptions = (LinearLayout) findViewById(R.id.extra_options_container);
 
+        mAddressAdapter = new EmailAddressAdapter(this);
+        mValidator = new Rfc822Validator("google.com"); //TODO use user's domain
+        mAttendeesList = initMultiAutoCompleteTextView(R.id.attendees, R.string.hint_attendees);
+
         mAllDayCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                 if (isChecked) {
@@ -664,7 +687,7 @@
                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
                     EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
                 }
-                
+
                 // Second pass: create the reminder spinners
                 reminderCursor.moveToPosition(-1);
                 while (reminderCursor.moveToNext()) {
@@ -684,7 +707,7 @@
             public void onClick(View v) {
                 addReminder();
             }
-        };        
+        };
         ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
         reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
 
@@ -696,33 +719,103 @@
             initFromIntent(intent);
         }
     }
-    
+
+    private Rfc822Token[] getAddressesFromList(MultiAutoCompleteTextView list) {
+        list.clearComposingText();
+        return Rfc822Tokenizer.tokenize(list.getText());
+    }
+
+    // From com.google.android.gm.ComposeActivity
+    private MultiAutoCompleteTextView initMultiAutoCompleteTextView(int res, int hintId) {
+        MultiAutoCompleteTextView list = (MultiAutoCompleteTextView) findViewById(res);
+        list.setAdapter(mAddressAdapter);
+        list.setTokenizer(new Rfc822Tokenizer());
+        list.setValidator(mValidator);
+
+        // NOTE: assumes no other filters are set
+        list.setFilters(sRecipientFilters);
+
+        return list;
+    }
+
+    /**
+     * From com.google.android.gm.ComposeActivity
+     * Implements special address cleanup rules:
+     * The first space key entry following an "@" symbol that is followed by any combination
+     * of letters and symbols, including one+ dots and zero commas, should insert an extra
+     * comma (followed by the space).
+     */
+    private static InputFilter[] sRecipientFilters = new InputFilter[] { new InputFilter() {
+
+        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
+                int dstart, int dend) {
+
+            // quick check - did they enter a single space?
+            if (end-start != 1 || source.charAt(start) != ' ') {
+                return null;
+            }
+
+            // determine if the characters before the new space fit the pattern
+            // follow backwards and see if we find a comma, dot, or @
+            int scanBack = dstart;
+            boolean dotFound = false;
+            while (scanBack > 0) {
+                char c = dest.charAt(--scanBack);
+                switch (c) {
+                    case '.':
+                        dotFound = true;    // one or more dots are req'd
+                        break;
+                    case ',':
+                        return null;
+                    case '@':
+                        if (!dotFound) {
+                            return null;
+                        }
+                        // we have found a comma-insert case.  now just do it
+                        // in the least expensive way we can.
+                        if (source instanceof Spanned) {
+                            SpannableStringBuilder sb = new SpannableStringBuilder(",");
+                            sb.append(source);
+                            return sb;
+                        } else {
+                            return ", ";
+                        }
+                    default:
+                        // just keep going
+                }
+            }
+
+            // no termination cases were found, so don't edit the input
+            return null;
+        }
+    }};
+
     private void initFromIntent(Intent intent) {
         String title = intent.getStringExtra(Events.TITLE);
         if (title != null) {
             mTitleTextView.setText(title);
         }
-        
+
         String location = intent.getStringExtra(Events.EVENT_LOCATION);
         if (location != null) {
             mLocationTextView.setText(location);
         }
-        
+
         String description = intent.getStringExtra(Events.DESCRIPTION);
         if (description != null) {
             mDescriptionTextView.setText(description);
         }
-        
+
         int availability = intent.getIntExtra(Events.TRANSPARENCY, -1);
         if (availability != -1) {
             mAvailabilitySpinner.setSelection(availability);
         }
-        
+
         int visibility = intent.getIntExtra(Events.VISIBILITY, -1);
         if (visibility != -1) {
             mVisibilitySpinner.setSelection(visibility);
         }
-        
+
         String rrule = intent.getStringExtra(Events.RRULE);
         if (rrule != null) {
             mRrule = rrule;
@@ -741,7 +834,7 @@
                 return;
             }
         }
-        
+
         if (mEventCursor != null) {
             Cursor cursor = mEventCursor;
             cursor.moveToFirst();
@@ -795,7 +888,7 @@
                                 } else if (which == 2) {
                                     mModification = MODIFY_ALL_FOLLOWING;
                                 }
-                                
+
                                 // If we are modifying all the events in a
                                 // series then disable and ignore the date.
                                 if (mModification == MODIFY_ALL) {
@@ -1069,7 +1162,7 @@
         LinearLayout parent = (LinearLayout) activity.findViewById(R.id.reminder_items_container);
         LinearLayout reminderItem = (LinearLayout) inflater.inflate(R.layout.edit_reminder_item, null);
         parent.addView(reminderItem);
-        
+
         Spinner spinner = (Spinner) reminderItem.findViewById(R.id.reminder_value);
         Resources res = activity.getResources();
         spinner.setPrompt(res.getString(R.string.reminders_label));
@@ -1077,7 +1170,7 @@
         ArrayAdapter<String> adapter = new ArrayAdapter<String>(activity, resource, labels);
         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
         spinner.setAdapter(adapter);
-        
+
         ImageButton reminderRemoveButton;
         reminderRemoveButton = (ImageButton) reminderItem.findViewById(R.id.reminder_remove);
         reminderRemoveButton.setOnClickListener(listener);
@@ -1088,17 +1181,17 @@
 
         return true;
     }
-    
+
     static void addMinutesToList(Context context, ArrayList<Integer> values,
             ArrayList<String> labels, int minutes) {
         int index = values.indexOf(minutes);
         if (index != -1) {
             return;
         }
-        
+
         // The requested "minutes" does not exist in the list, so insert it
         // into the list.
-        
+
         String label = constructReminderLabel(context, minutes, false);
         int len = values.size();
         for (int i = 0; i < len; i++) {
@@ -1108,14 +1201,14 @@
                 return;
             }
         }
-        
+
         values.add(minutes);
         labels.add(len, label);
     }
-    
+
     /**
      * Finds the index of the given "minutes" in the "values" list.
-     * 
+     *
      * @param values the list of minutes corresponding to the spinner choices
      * @param minutes the minutes to search for in the values list
      * @return the index of "minutes" in the "values" list
@@ -1129,7 +1222,7 @@
         }
         return index;
     }
-    
+
     // Constructs a label given an arbitrary number of minutes.  For example,
     // if the given minutes is 63, then this returns the string "63 minutes".
     // As another example, if the given minutes is 120, then this returns
@@ -1137,7 +1230,7 @@
     static String constructReminderLabel(Context context, int minutes, boolean abbrev) {
         Resources resources = context.getResources();
         int value, resId;
-        
+
         if (minutes % 60 != 0) {
             value = minutes;
             if (abbrev) {
@@ -1185,7 +1278,7 @@
     // Saves the event.  Returns true if it is okay to exit this activity.
     private boolean save() {
         boolean forceSaveReminders = false;
-        
+
         // If we are creating a new event, then make sure we wait until the
         // query to fetch the list of calendars has finished.
         if (mEventCursor == null) {
@@ -1215,7 +1308,9 @@
             Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
         }
 
-        ContentResolver cr = getContentResolver();
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+        int eventIdIndex = -1;
+
         ContentValues values = getContentValuesFromUi();
         Uri uri = mUri;
 
@@ -1224,21 +1319,23 @@
         if (uri == null) {
             // Create new event with new contents
             addRecurrenceRule(values);
-            uri = cr.insert(Events.CONTENT_URI, values);
+            eventIdIndex = ops.size();
+            Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
+            ops.add(b.build());
             forceSaveReminders = true;
 
         } else if (mRrule == null) {
             // Modify contents of a non-repeating event
             addRecurrenceRule(values);
             checkTimeDependentFields(values);
-            cr.update(uri, values, null, null);
-            
+            ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+
         } else if (mInitialValues.getAsString(Events.RRULE) == null) {
             // This event was changed from a non-repeating event to a
             // repeating event.
             addRecurrenceRule(values);
             values.remove(Events.DTEND);
-            cr.update(uri, values, null, null);
+            ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
 
         } else if (mModification == MODIFY_SELECTED) {
             // Modify contents of the current instance of repeating event
@@ -1250,7 +1347,9 @@
             boolean allDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
             values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
 
-            uri = cr.insert(Events.CONTENT_URI, values);
+            eventIdIndex = ops.size();
+            Builder b = ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values);
+            ops.add(b.build());
             forceSaveReminders = true;
 
         } else if (mModification == MODIFY_ALL_FOLLOWING) {
@@ -1263,56 +1362,127 @@
                 // then delete the whole series.  Otherwise, update the series
                 // to end at the new start time.
                 if (isFirstEventInSeries()) {
-                    cr.delete(uri, null, null);
+                    ops.add(ContentProviderOperation.newDelete(uri).build());
                 } else {
                     // Update the current repeating event to end at the new
                     // start time.
-                    updatePastEvents(cr, uri);
+                    updatePastEvents(ops, uri);
                 }
-                uri = cr.insert(Events.CONTENT_URI, values);
+                eventIdIndex = ops.size();
+                ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
+                        .build());
             } else {
                 if (isFirstEventInSeries()) {
                     checkTimeDependentFields(values);
                     values.remove(Events.DTEND);
-                    cr.update(uri, values, null, null);
+                    Builder b = ContentProviderOperation.newUpdate(uri).withValues(values);
+                    ops.add(b.build());
                 } else {
                     // Update the current repeating event to end at the new
                     // start time.
-                    updatePastEvents(cr, uri);
+                    updatePastEvents(ops, uri);
 
                     // Create a new event with the user-modified fields
                     values.remove(Events.DTEND);
-                    uri = cr.insert(Events.CONTENT_URI, values);
+                    eventIdIndex = ops.size();
+                    ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(
+                            values).build());
                 }
             }
             forceSaveReminders = true;
 
         } else if (mModification == MODIFY_ALL) {
-            
+
             // Modify all instances of repeating event
             addRecurrenceRule(values);
-            
+
             if (mRrule == null) {
                 // We've changed a recurring event to a non-recurring event.
                 // Delete the whole series and replace it with a new
                 // non-recurring event.
-                cr.delete(uri, null, null);
-                uri = cr.insert(Events.CONTENT_URI, values);
+                ops.add(ContentProviderOperation.newDelete(uri).build());
+
+                eventIdIndex = ops.size();
+                ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI).withValues(values)
+                        .build());
                 forceSaveReminders = true;
             } else {
                 checkTimeDependentFields(values);
                 values.remove(Events.DTEND);
-                cr.update(uri, values, null, null);
+                ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
             }
         }
 
-        if (uri != null) {
+        ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
+                mReminderValues);
+        if (eventIdIndex != -1) {
+            saveRemindersWithBackRef(ops, eventIdIndex, reminderMinutes, mOriginalMinutes,
+                    forceSaveReminders);
+        } else if (uri != null) {
             long eventId = ContentUris.parseId(uri);
-            ArrayList<Integer> reminderMinutes = reminderItemsToMinutes(mReminderItems,
-                    mReminderValues);
-            saveReminders(cr, eventId, reminderMinutes, mOriginalMinutes,
+            saveReminders(ops, eventId, reminderMinutes, mOriginalMinutes,
                     forceSaveReminders);
         }
+
+        if (eventIdIndex != -1 || uri != null) {
+            // Delete all the existing attendees for this event
+            Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+
+            long eventId = -1;
+            if (eventIdIndex == -1) {
+                eventId = ContentUris.parseId(uri);
+                String where = Attendees.EVENT_ID + "=?";
+                String[] args = new String[] {
+                    Long.toString(eventId)
+                };
+                b.withSelection(where, args);
+            } else {
+                // Delete all the existing reminders for this event
+                b.withSelection(Attendees.EVENT_ID + "=?", new String[1]);
+                b.withSelectionBackReference(0, eventIdIndex);
+            }
+            ops.add(b.build());
+
+            if (mAttendeesList.getText().length() > 0) {
+                Rfc822Token[] attendees = getAddressesFromList(mAttendeesList);
+                // Insert the attendees
+                for (Rfc822Token attendee : attendees) {
+                    values.clear();
+                    values.put(Attendees.ATTENDEE_NAME, attendee.getName());
+                    values.put(Attendees.ATTENDEE_EMAIL, attendee.getAddress());
+                    values.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+                    values.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+                    values.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_NONE);
+
+                    if (eventIdIndex != -1) {
+                        b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
+                                .withValues(values);
+                        b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
+                    } else {
+                        values.put(Attendees.EVENT_ID, eventId);
+                        b = ContentProviderOperation.newInsert(Attendees.CONTENT_URI)
+                                .withValues(values);
+                    }
+                    ops.add(b.build());
+                }
+            }
+        }
+
+        try {
+            // TODO Move this to background thread
+            ContentProviderResult[] results =
+                getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops);
+            if (DEBUG) {
+                Log.v("=====", "results = " + Arrays.toString(results));
+            }
+        } catch (RemoteException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (OperationApplicationException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+
         return true;
     }
 
@@ -1322,7 +1492,7 @@
         return start == mStartTime.toMillis(true);
     }
 
-    private void updatePastEvents(ContentResolver cr, Uri uri) {
+    private void updatePastEvents(ArrayList<ContentProviderOperation> ops, Uri uri) {
         long oldStartMillis = mEventCursor.getLong(EVENT_INDEX_DTSTART);
         String oldDuration = mEventCursor.getString(EVENT_INDEX_DURATION);
         boolean allDay = mEventCursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
@@ -1338,17 +1508,17 @@
         // must include just the date field, and not the time field.  The
         // repeating events repeat up to and including the "until" time.
         untilTime.timezone = Time.TIMEZONE_UTC;
-        
+
         // Subtract one second from the old begin time to get the new
         // "until" time.
-        untilTime.set(begin - 1000);  // subtract one second (1000 millis) 
+        untilTime.set(begin - 1000);  // subtract one second (1000 millis)
         if (allDay) {
             untilTime.hour = 0;
             untilTime.minute = 0;
             untilTime.second = 0;
             untilTime.allDay = true;
             untilTime.normalize(false);
-            
+
             // For all-day events, the duration must be in days, not seconds.
             // Otherwise, Google Calendar will (mistakenly) change this event
             // into a non-all-day event.
@@ -1364,7 +1534,8 @@
         oldValues.put(Events.DTSTART, oldStartMillis);
         oldValues.put(Events.DURATION, oldDuration);
         oldValues.put(Events.RRULE, mEventRecurrence.toString());
-        cr.update(uri, oldValues, null, null);
+        Builder b = ContentProviderOperation.newUpdate(uri).withValues(oldValues);
+        ops.add(b.build());
     }
 
     private void checkTimeDependentFields(ContentValues values) {
@@ -1373,13 +1544,13 @@
         boolean oldAllDay = mInitialValues.getAsInteger(Events.ALL_DAY) != 0;
         String oldRrule = mInitialValues.getAsString(Events.RRULE);
         String oldTimezone = mInitialValues.getAsString(Events.EVENT_TIMEZONE);
-        
+
         long newBegin = values.getAsLong(Events.DTSTART);
         long newEnd = values.getAsLong(Events.DTEND);
         boolean newAllDay = values.getAsInteger(Events.ALL_DAY) != 0;
         String newRrule = values.getAsString(Events.RRULE);
         String newTimezone = values.getAsString(Events.EVENT_TIMEZONE);
-        
+
         // If none of the time-dependent fields changed, then remove them.
         if (oldBegin == newBegin && oldEnd == newEnd && oldAllDay == newAllDay
                 && TextUtils.equals(oldRrule, newRrule)
@@ -1414,7 +1585,7 @@
             values.put(Events.DTSTART, oldStartMillis);
         }
     }
-    
+
     static ArrayList<Integer> reminderItemsToMinutes(ArrayList<LinearLayout> reminderItems,
             ArrayList<Integer> reminderValues) {
         int len = reminderItems.size();
@@ -1431,8 +1602,8 @@
     /**
      * Saves the reminders, if they changed.  Returns true if the database
      * was updated.
-     * 
-     * @param cr the ContentResolver
+     *
+     * @param ops the array of ContentProviderOperations
      * @param eventId the id of the event whose reminders are being updated
      * @param reminderMinutes the array of reminders set by the user
      * @param originalMinutes the original array of reminders
@@ -1440,7 +1611,7 @@
      *   change
      * @return true if the database was updated
      */
-    static boolean saveReminders(ContentResolver cr, long eventId,
+    static boolean saveReminders(ArrayList<ContentProviderOperation> ops, long eventId,
             ArrayList<Integer> reminderMinutes, ArrayList<Integer> originalMinutes,
             boolean forceSave) {
         // If the reminders have not changed, then don't update the database
@@ -1448,28 +1619,70 @@
             return false;
         }
 
-        // Delete all the existing reminders for this event
-        String where = Reminders.EVENT_ID + "=?";
-        String[] args = new String[] { Long.toString(eventId) };
-        cr.delete(Reminders.CONTENT_URI, where, args);
+        // TODO re-enable this
+//        // Delete all the existing reminders for this event
+//        String where = Reminders.EVENT_ID + "=?";
+//        String[] args = new String[] { Long.toString(eventId) };
+//        Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+//        b.withSelection(where, args);
+//        ops.add(b.build());
+//
+//        // Update the "hasAlarm" field for the event
+//        ContentValues values = new ContentValues();
+//        int len = reminderMinutes.size();
+//        values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
+//        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
+//        ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
+//
+//        // Insert the new reminders, if any
+//        for (int i = 0; i < len; i++) {
+//            int minutes = reminderMinutes.get(i);
+//
+//            values.clear();
+//            values.put(Reminders.MINUTES, minutes);
+//            values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
+//            values.put(Reminders.EVENT_ID, eventId);
+//            b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
+//            ops.add(b.build());
+//        }
+        return true;
+    }
 
-        // Update the "hasAlarm" field for the event
-        ContentValues values = new ContentValues();
-        int len = reminderMinutes.size();
-        values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
-        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
-        cr.update(uri, values, null /* where */, null /* selection args */);
-
-        // Insert the new reminders, if any
-        for (int i = 0; i < len; i++) {
-            int minutes = reminderMinutes.get(i);
-
-            values.clear();
-            values.put(Reminders.MINUTES, minutes);
-            values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
-            values.put(Reminders.EVENT_ID, eventId);
-            cr.insert(Reminders.CONTENT_URI, values);
+    static boolean saveRemindersWithBackRef(ArrayList<ContentProviderOperation> ops,
+            int eventIdIndex, ArrayList<Integer> reminderMinutes,
+            ArrayList<Integer> originalMinutes, boolean forceSave) {
+        // If the reminders have not changed, then don't update the database
+        if (reminderMinutes.equals(originalMinutes) && !forceSave) {
+            return false;
         }
+
+        // TODO re-enable this
+//        // Delete all the existing reminders for this event
+//        Builder b = ContentProviderOperation.newDelete(Reminders.CONTENT_URI);
+//        b.withSelection(Reminders.EVENT_ID + "=?", new String[1]);
+//        b.withSelectionBackReference(0, eventIdIndex);
+//        ops.add(b.build());
+//
+//        // Update the "hasAlarm" field for the event
+//        ContentValues values = new ContentValues();
+//        int len = reminderMinutes.size();
+//        values.put(Events.HAS_ALARM, (len > 0) ? 1 : 0);
+//        b = ContentProviderOperation.newUpdate(Events.CONTENT_URI).withValues(values);
+//        b.withSelection(Events._ID + "=?", new String[1]);
+//        b.withSelectionBackReference(0, eventIdIndex);
+//        ops.add(b.build());
+//
+//        // Insert the new reminders, if any
+//        for (int i = 0; i < len; i++) {
+//            int minutes = reminderMinutes.get(i);
+//
+//            values.clear();
+//            values.put(Reminders.MINUTES, minutes);
+//            values.put(Reminders.METHOD, Reminders.METHOD_ALERT);
+//            b = ContentProviderOperation.newInsert(Reminders.CONTENT_URI).withValues(values);
+//            b.withValueBackReference(Reminders.EVENT_ID, eventIdIndex);
+//            ops.add(b.build());
+//        }
         return true;
     }
 
@@ -1479,7 +1692,7 @@
         if (mRrule == null) {
             return;
         }
-        
+
         values.put(Events.RRULE, mRrule);
         long end = mEndTime.toMillis(true /* ignore dst */);
         long start = mStartTime.toMillis(true /* ignore dst */);
@@ -1599,7 +1812,7 @@
             mEndTime.monthDay++;
             mEndTime.timezone = timezone;
             endMillis = mEndTime.normalize(true);
-            
+
             if (mEventCursor == null) {
                 // This is a new event
                 calendarId = mCalendarsSpinner.getSelectedItemId();
@@ -1612,7 +1825,7 @@
             if (mEventCursor != null) {
                 // This is an existing event
                 timezone = mEventCursor.getString(EVENT_INDEX_TIMEZONE);
-                
+
                 // The timezone might be null if we are changing an existing
                 // all-day event to a non-all-day event.  We need to assign
                 // a timezone to the non-all-day event.
@@ -1623,7 +1836,7 @@
             } else {
                 // This is a new event
                 calendarId = mCalendarsSpinner.getSelectedItemId();
-                
+
                 // The timezone for a new event is the currently displayed
                 // timezone, NOT the timezone of the containing calendar.
                 timezone = TimeZone.getDefault().getID();
diff --git a/src/com/android/calendar/EmailAddressAdapter.java b/src/com/android/calendar/EmailAddressAdapter.java
new file mode 100644
index 0000000..b0b58ba
--- /dev/null
+++ b/src/com/android/calendar/EmailAddressAdapter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007 Google Inc.
+ *
+ * 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.calendar;
+
+import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.People;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.view.View;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+public class EmailAddressAdapter extends ResourceCursorAdapter {
+    public static final int NAME_INDEX = 1;
+    public static final int DATA_INDEX = 2;
+    
+    private static final String SORT_ORDER = People.TIMES_CONTACTED + " DESC, " + People.NAME;
+    private ContentResolver mContentResolver;
+    
+    private static final String[] PROJECTION = {
+        ContactMethods._ID,     // 0
+        ContactMethods.NAME,    // 1
+        ContactMethods.DATA     // 2
+    };
+
+    public EmailAddressAdapter(Context context) {
+        super(context, android.R.layout.simple_dropdown_item_1line, null);
+        mContentResolver = context.getContentResolver();
+    }
+
+    @Override
+    public final String convertToString(Cursor cursor) {
+        String name = cursor.getString(NAME_INDEX);
+        String address = cursor.getString(DATA_INDEX);
+
+        return new Rfc822Token(name, address, null).toString();
+    }
+
+    private final String makeDisplayString(Cursor cursor) {
+        StringBuilder s = new StringBuilder();
+        boolean flag = false;
+        String name = cursor.getString(NAME_INDEX);
+        String address = cursor.getString(DATA_INDEX);
+
+        if (!TextUtils.isEmpty(name)) {
+            s.append(name);
+            flag = true;
+        }
+        
+        if (flag) {
+            s.append(" <");
+        }
+        
+        s.append(address);
+
+        if (flag) {
+            s.append(">");
+        }
+        
+        return s.toString();
+    }
+    
+    @Override
+    public final void bindView(View view, Context context, Cursor cursor) {
+        ((TextView) view).setText(makeDisplayString(cursor));
+    }
+
+    @Override
+    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+        String where = null;
+
+        if (constraint != null) {
+            String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%');
+            
+            StringBuilder s = new StringBuilder();
+            s.append("(people.name LIKE ");
+            s.append(filter);
+            s.append(") OR (contact_methods.data LIKE ");
+            s.append(filter);
+            s.append(")");
+            
+            where = s.toString();
+        }
+
+        return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER);
+    }
+}
diff --git a/src/com/android/calendar/EventInfoActivity.java b/src/com/android/calendar/EventInfoActivity.java
index 56eebfe..4c7db60 100644
--- a/src/com/android/calendar/EventInfoActivity.java
+++ b/src/com/android/calendar/EventInfoActivity.java
@@ -21,35 +21,51 @@
 import static android.provider.Calendar.AttendeesColumns.ATTENDEE_STATUS;
 
 import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
+import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.PorterDuff;
+import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.RemoteException;
+import android.pim.ContactsAsyncHelper;
 import android.pim.EventRecurrence;
 import android.preference.PreferenceManager;
 import android.provider.Calendar;
+import android.provider.Contacts;
 import android.provider.Calendar.Attendees;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
 import android.provider.Calendar.Reminders;
+import android.provider.Contacts.ContactMethods;
+import android.provider.Contacts.Intents;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Presence;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.text.format.Time;
 import android.text.util.Linkify;
+import android.text.util.Rfc822Token;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.ImageButton;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.Spinner;
 import android.widget.TextView;
@@ -57,10 +73,12 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.regex.Pattern;
 
 public class EventInfoActivity extends Activity implements View.OnClickListener,
         AdapterView.OnItemSelectedListener {
+    private static final String TAG = "EventInfoActivity";
     private static final int MAX_REMINDERS = 5;
 
     /**
@@ -100,12 +118,17 @@
 
     private static final String[] ATTENDEES_PROJECTION = new String[] {
         Attendees._ID,                      // 0
-        Attendees.ATTENDEE_RELATIONSHIP,    // 1
-        Attendees.ATTENDEE_STATUS,          // 2
+        Attendees.ATTENDEE_NAME,            // 1
+        Attendees.ATTENDEE_EMAIL,           // 2
+        Attendees.ATTENDEE_RELATIONSHIP,    // 3
+        Attendees.ATTENDEE_STATUS,          // 4
     };
     private static final int ATTENDEES_INDEX_ID = 0;
-    private static final int ATTENDEES_INDEX_RELATIONSHIP = 1;
-    private static final int ATTENDEES_INDEX_STATUS = 2;
+    private static final int ATTENDEES_INDEX_NAME = 1;
+    private static final int ATTENDEES_INDEX_EMAIL = 2;
+    private static final int ATTENDEES_INDEX_RELATIONSHIP = 3;
+    private static final int ATTENDEES_INDEX_STATUS = 4;
+
     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
 
     static final String[] CALENDARS_PROJECTION = new String[] {
@@ -168,6 +191,41 @@
     private boolean mIsRepeating;
 
     private Pattern mWildcardPattern = Pattern.compile("^.*$");
+    private LayoutInflater mLayoutInflater;
+
+    private static class ViewHolder {
+        ImageView avatar;
+        ImageView presence;
+    }
+    private HashMap<String, ViewHolder> mPresenceStatuses = new HashMap<String, ViewHolder>();
+    private PresenceQueryHandler mPresenceQueryHandler;
+
+    static final String[] PEOPLE_PROJECTION = new String[] {
+        People._ID,
+    };
+
+    Uri CONTACT_PRESENCE_URI = Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI,
+            "with_presence");
+    int PRESENCE_PROJECTION_EMAIL_INDEX = 1;
+    int PRESENCE_PROJECTION_PRESENCE_INDEX = 2;
+    private static final String[] PRESENCE_PROJECTION = new String[] {
+        ContactMethods._ID,         // 0
+        ContactMethods.DATA,        // 1
+        People.PRESENCE_STATUS,     // 2
+    };
+
+    ArrayList<Attendee> mAcceptedAttendees = new ArrayList<Attendee>();
+    ArrayList<Attendee> mDeclinedAttendees = new ArrayList<Attendee>();
+    ArrayList<Attendee> mTentativeAttendees = new ArrayList<Attendee>();
+    private OnClickListener contactOnClickListener = new OnClickListener() {
+        public void onClick(View v) {
+            Attendee attendee = (Attendee) v.getTag();
+            Rect rect = new Rect();
+            v.getDrawingRect(rect);
+            showContactInfo(attendee, rect);
+        }
+    };
+    private int mColor;
 
     // This is called when one of the "remove reminder" buttons is selected.
     public void onClick(View v) {
@@ -177,27 +235,27 @@
         mReminderItems.remove(reminderItem);
         updateRemindersVisibility();
     }
-    
+
     public void onItemSelected(AdapterView parent, View v, int position, long id) {
         // If they selected the "No response" option, then don't display the
         // dialog asking which events to change.
         if (id == 0 && mResponseOffset == 0) {
             return;
         }
-        
+
         // If this is not a repeating event, then don't display the dialog
         // asking which events to change.
         if (!mIsRepeating) {
             return;
         }
-        
+
         // If the selection is the same as the original, then don't display the
         // dialog asking which events to change.
         int index = findResponseIndexFor(mOriginalAttendeeResponse);
         if (position == index + mResponseOffset) {
             return;
         }
-        
+
         // This is a repeating event. We need to ask the user if they mean to
         // change just this one instance or all instances.
         mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
@@ -230,7 +288,6 @@
         Uri uri = Attendees.CONTENT_URI;
         String where = String.format(ATTENDEES_WHERE, mEventId);
         mAttendeesCursor = managedQuery(uri, ATTENDEES_PROJECTION, where, null);
-        initAttendeesCursor();
 
         // Calendars cursor
         uri = Calendars.CONTENT_URI;
@@ -279,7 +336,7 @@
                     int minutes = reminderCursor.getInt(REMINDERS_INDEX_MINUTES);
                     EditEvent.addMinutesToList(this, mReminderValues, mReminderLabels, minutes);
                 }
-                
+
                 // Second pass: create the reminder spinners
                 reminderCursor.moveToPosition(-1);
                 while (reminderCursor.moveToNext()) {
@@ -301,12 +358,15 @@
             public void onClick(View v) {
                 addReminder();
             }
-        };        
+        };
         ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
         reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
 
         mDeleteEventHelper = new DeleteEventHelper(this, true /* exit when done */);
         mEditResponseHelper = new EditResponseHelper(this);
+
+        mPresenceQueryHandler = new PresenceQueryHandler(this, cr);
+        mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     }
 
     @Override
@@ -319,6 +379,7 @@
         }
         initAttendeesCursor();
         initCalendarsCursor();
+        updateResponse();
     }
 
     /**
@@ -338,10 +399,47 @@
         return false;
     }
 
+    private static class Attendee {
+        String mName;
+        String mEmail;
+
+        Attendee(String name, String email) {
+            mName = name;
+            mEmail = email;
+        }
+    }
+
     private void initAttendeesCursor() {
         if (mAttendeesCursor != null) {
             if (mAttendeesCursor.moveToFirst()) {
+                mAcceptedAttendees.clear();
+                mDeclinedAttendees.clear();
+                mTentativeAttendees.clear();
+
+                /*
+                 * TODO: We have been relying on the fact that "our user" appears
+                 * in the first row. The right way is to look up the email addr
+                 * associated with the calendar and do a match here.
+                 */
                 mRelationship = mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+                do {
+                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
+                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
+                    switch(status) {
+                        case Attendees.ATTENDEE_STATUS_ACCEPTED:
+                            mAcceptedAttendees.add(new Attendee(name, email));
+                            break;
+                        case Attendees.ATTENDEE_STATUS_DECLINED:
+                            mDeclinedAttendees.add(new Attendee(name, email));
+                            break;
+                        default:
+                            mTentativeAttendees.add(new Attendee(name, email));
+                    }
+                } while (mAttendeesCursor.moveToNext());
+                mAttendeesCursor.moveToFirst();
+
+                updateAttendees();
             }
         }
     }
@@ -361,8 +459,19 @@
         ContentResolver cr = getContentResolver();
         ArrayList<Integer> reminderMinutes = EditEvent.reminderItemsToMinutes(mReminderItems,
                 mReminderValues);
-        boolean changed = EditEvent.saveReminders(cr, mEventId, reminderMinutes, mOriginalMinutes,
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(3);
+        boolean changed = EditEvent.saveReminders(ops, mEventId, reminderMinutes, mOriginalMinutes,
                 false /* no force save */);
+        try {
+            cr.applyBatch(Calendars.CONTENT_URI.getAuthority(), ops);
+        } catch (RemoteException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (OperationApplicationException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+
         changed |= saveResponse(cr);
         if (changed) {
             Toast.makeText(this, R.string.saving_event, Toast.LENGTH_SHORT).show();
@@ -414,7 +523,7 @@
 
         return super.onPrepareOptionsMenu(menu);
     }
-    
+
     private void addReminder() {
         // TODO: when adding a new reminder, make it different from the
         // last one in the list (if any).
@@ -465,7 +574,7 @@
     /**
      * Saves the response to an invitation if the user changed the response.
      * Returns true if the database was updated.
-     * 
+     *
      * @param cr the ContentResolver
      * @return true if the database was changed
      */
@@ -510,7 +619,7 @@
         }
         return false;
     }
-    
+
     private void updateResponse(ContentResolver cr, long eventId, long attendeeId, int status) {
         // Update the "selfAttendeeStatus" field for the event
         ContentValues values = new ContentValues();
@@ -522,7 +631,7 @@
         Uri uri = ContentUris.withAppendedId(Attendees.CONTENT_URI, attendeeId);
         cr.update(uri, values, null /* where */, null /* selection args */);
     }
-    
+
     private void createExceptionResponse(ContentResolver cr, long eventId,
             long attendeeId, int status) {
         // Fetch information about the repeating event.
@@ -535,13 +644,13 @@
         try {
             cursor.moveToFirst();
             ContentValues values = new ContentValues();
-            
+
             String title = cursor.getString(EVENT_INDEX_TITLE);
             String timezone = cursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
             int calendarId = cursor.getInt(EVENT_INDEX_CALENDAR_ID);
             boolean allDay = cursor.getInt(EVENT_INDEX_ALL_DAY) != 0;
             String syncId = cursor.getString(EVENT_INDEX_SYNC_ID);
-            
+
             values.put(Events.TITLE, title);
             values.put(Events.EVENT_TIMEZONE, timezone);
             values.put(Events.ALL_DAY, allDay ? 1 : 0);
@@ -553,7 +662,7 @@
             values.put(Events.ORIGINAL_ALL_DAY, allDay ? 1 : 0);
             values.put(Events.STATUS, Events.STATUS_CONFIRMED);
             values.put(Events.SELF_ATTENDEE_STATUS, status);
-            
+
             // Create a recurrence exception
             cr.insert(Events.CONTENT_URI, values);
         } finally {
@@ -602,17 +711,17 @@
         String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
         boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
         String eventTimezone = mEventCursor.getString(EVENT_INDEX_EVENT_TIMEZONE);
-        int color = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
+        mColor = mEventCursor.getInt(EVENT_INDEX_COLOR) & 0xbbffffff;
 
         View calBackground = findViewById(R.id.cal_background);
-        calBackground.setBackgroundColor(color);
+        calBackground.setBackgroundColor(mColor);
 
         TextView title = (TextView) findViewById(R.id.title);
-        title.setTextColor(color);
-        
-        View divider = (View) findViewById(R.id.divider);
-        divider.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
-        
+        title.setTextColor(mColor);
+
+        View divider = findViewById(R.id.divider);
+        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
         // What
         if (eventName != null) {
             setTextCommon(R.id.title, eventName);
@@ -687,9 +796,125 @@
         } else {
             setVisibilityCommon(R.id.calendar_container, View.GONE);
         }
+    }
 
-        // Response
-        updateResponse();
+    private void updateAttendees() {
+        CharSequence[] entries;
+        entries = getResources().getTextArray(R.array.response_labels2);
+        LinearLayout attendeesLayout = (LinearLayout) findViewById(R.id.attendee_list);
+        attendeesLayout.removeAllViewsInLayout();
+        addAttendeesToLayout(mAcceptedAttendees, attendeesLayout, entries[0]);
+        addAttendeesToLayout(mDeclinedAttendees, attendeesLayout, entries[2]);
+        addAttendeesToLayout(mTentativeAttendees, attendeesLayout, entries[1]);
+    }
+
+    private void addAttendeesToLayout(ArrayList<Attendee> attendees, LinearLayout attendeeList,
+            CharSequence sectionTitle) {
+        if (attendees.size() == 0) {
+            return;
+        }
+
+        ContentResolver cr = getContentResolver();
+        // Yes/No/Maybe Title
+        View titleView = mLayoutInflater.inflate(R.layout.contact_item, null);
+        titleView.findViewById(R.id.avatar).setVisibility(View.GONE);
+        View divider = titleView.findViewById(R.id.separator);
+        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
+        TextView title = (TextView) titleView.findViewById(R.id.name);
+        title.setText(getString(R.string.response_label, sectionTitle, attendees.size()));
+        title.setTextAppearance(this, R.style.TextAppearance_EventInfo_Label);
+        attendeeList.addView(titleView);
+
+        // Attendees
+        int numOfAttendees = attendees.size();
+        StringBuilder selection = new StringBuilder(Contacts.ContactMethods.DATA + " IN (");
+        String[] selectionArgs = new String[numOfAttendees];
+
+        for (int i = 0; i < numOfAttendees; ++i) {
+            Attendee attendee = attendees.get(i);
+            selectionArgs[i] = attendee.mEmail;
+
+            View v = mLayoutInflater.inflate(R.layout.contact_item, null);
+            v.setOnClickListener(contactOnClickListener);
+            v.setTag(attendee);
+
+            View separator = v.findViewById(R.id.separator);
+            separator.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+
+            // Text
+            TextView tv = (TextView) v.findViewById(R.id.name);
+            String name = attendee.mName;
+            if (name == null || name.length() == 0) {
+                name = attendee.mEmail;
+            }
+            tv.setText(name);
+
+            ViewHolder vh = new ViewHolder();
+            vh.avatar = (ImageView) v.findViewById(R.id.avatar);
+            vh.presence = (ImageView) v.findViewById(R.id.presence);
+            mPresenceStatuses.put(attendee.mEmail, vh);
+
+            if (i == 0) {
+                selection.append('?');
+            } else {
+                selection.append(", ?");
+            }
+
+            attendeeList.addView(v);
+        }
+        selection.append(')');
+
+        mPresenceQueryHandler.startQuery(0, attendees, CONTACT_PRESENCE_URI, PRESENCE_PROJECTION,
+                selection.toString(), selectionArgs, null);
+    }
+
+    private class PresenceQueryHandler extends AsyncQueryHandler {
+        Context mContext;
+        ContentResolver mContentResolver;
+
+        public PresenceQueryHandler(Context context, ContentResolver cr) {
+            super(cr);
+            mContentResolver = cr;
+            mContext = context;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
+                ViewHolder vh = mPresenceStatuses.get(email);
+                ImageView presenceView = vh.presence;
+                if (presenceView != null) {
+                    int status = cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX);
+                    presenceView.setImageResource(Presence.getPresenceIconResourceId(status));
+                    presenceView.setVisibility(View.VISIBLE);
+                }
+            }
+
+            ArrayList<Attendee> attendees = (ArrayList<Attendee>) cookie;
+            for (Attendee attendee : attendees) {
+                Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, Uri
+                        .encode(attendee.mEmail));
+                // TODO Get rid of this query.
+                Cursor personCursor = mContentResolver.query(uri, PEOPLE_PROJECTION, null, null,
+                        null);
+                if (personCursor != null) {
+                    if (personCursor.moveToFirst()) {
+                        Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, personCursor
+                                .getInt(0));
+                        ViewHolder vh = mPresenceStatuses.get(attendee.mEmail);
+                        if (vh != null) {
+                            ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(mContext,
+                                    vh.avatar, personUri, -1);
+                        }
+                    }
+                    personCursor.close();
+                }
+            }
+        }
     }
 
     void updateResponse() {
@@ -751,4 +976,32 @@
         }
         return;
     }
+
+    /**
+     * Taken from com.google.android.gm.HtmlConversationActivity
+     *
+     * Send the intent that shows the Contact info corresponding to the email address.
+     */
+    public void showContactInfo(Attendee attendee, Rect rect) {
+        Uri contactUri = Uri.fromParts("mailto", attendee.mEmail, null);
+
+        Intent contactIntent = new Intent(Contacts.Intents.SHOW_OR_CREATE_CONTACT);
+        contactIntent.setData(contactUri);
+
+        // Pass along full E-mail string for possible create dialog
+        Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
+        contactIntent.putExtra(Contacts.Intents.EXTRA_CREATE_DESCRIPTION,
+                sender.toString());
+
+        // Mark target position using on-screen coordinates
+        // TODO uncomment when contacts code is in.
+        // contactIntent.putExtra(Intents.EXTRA_TARGET_RECT, rect);
+
+        // Only provide personal name hint if we have one
+        if (attendee.mName != null && attendee.mName.length() > 0) {
+            contactIntent.putExtra(Intents.Insert.NAME, attendee.mName);
+        }
+
+        startActivity(contactIntent);
+    }
 }