OSDN Git Service

b/1946035 Fixed the crash that happen when the user clicks on the where field but...
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / EventInfoActivity.java
index 2a17c89..ced0f92 100644 (file)
@@ -18,63 +18,100 @@ package com.android.calendar;
 
 import static android.provider.Calendar.EVENT_BEGIN_TIME;
 import static android.provider.Calendar.EVENT_END_TIME;
+import static android.provider.Calendar.AttendeesColumns.ATTENDEE_STATUS;
 
 import android.app.Activity;
+import android.content.ActivityNotFoundException;
+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.MotionEvent;
 import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+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;
+import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
 
-public class EventInfoActivity extends Activity implements View.OnClickListener {
+public class EventInfoActivity extends Activity implements View.OnClickListener,
+        AdapterView.OnItemSelectedListener {
     private static final int MAX_REMINDERS = 5;
 
+    /**
+     * These are the corresponding indices into the array of strings
+     * "R.array.change_response_labels" in the resource file.
+     */
+    static final int UPDATE_SINGLE = 0;
+    static final int UPDATE_ALL = 1;
+
     private static final String[] EVENT_PROJECTION = new String[] {
-        Events._ID,             // 0  do not remove; used in DeleteEventHelper
-        Events.TITLE,           // 1  do not remove; used in DeleteEventHelper
-        Events.RRULE,           // 2  do not remove; used in DeleteEventHelper
-        Events.ALL_DAY,         // 3  do not remove; used in DeleteEventHelper
-        Events.CALENDAR_ID,     // 4  do not remove; used in DeleteEventHelper
-        Events.DTSTART,         // 5  do not remove; used in DeleteEventHelper
-        Events._SYNC_ID,        // 6  do not remove; used in DeleteEventHelper
-        Events.EVENT_TIMEZONE,  // 7  do not remove; used in DeleteEventHelper
-        Events.DESCRIPTION,     // 8
-        Events.EVENT_LOCATION,  // 9
-        Events.HAS_ALARM,       // 10
-        Events.ACCESS_LEVEL,    // 11
-        Events.COLOR,           // 12
+        Events._ID,                  // 0  do not remove; used in DeleteEventHelper
+        Events.TITLE,                // 1  do not remove; used in DeleteEventHelper
+        Events.RRULE,                // 2  do not remove; used in DeleteEventHelper
+        Events.ALL_DAY,              // 3  do not remove; used in DeleteEventHelper
+        Events.CALENDAR_ID,          // 4  do not remove; used in DeleteEventHelper
+        Events.DTSTART,              // 5  do not remove; used in DeleteEventHelper
+        Events._SYNC_ID,             // 6  do not remove; used in DeleteEventHelper
+        Events.EVENT_TIMEZONE,       // 7  do not remove; used in DeleteEventHelper
+        Events.DESCRIPTION,          // 8
+        Events.EVENT_LOCATION,       // 9
+        Events.HAS_ALARM,            // 10
+        Events.ACCESS_LEVEL,         // 11
+        Events.COLOR,                // 12
     };
     private static final int EVENT_INDEX_ID = 0;
     private static final int EVENT_INDEX_TITLE = 1;
     private static final int EVENT_INDEX_RRULE = 2;
     private static final int EVENT_INDEX_ALL_DAY = 3;
     private static final int EVENT_INDEX_CALENDAR_ID = 4;
+    private static final int EVENT_INDEX_SYNC_ID = 6;
     private static final int EVENT_INDEX_EVENT_TIMEZONE = 7;
     private static final int EVENT_INDEX_DESCRIPTION = 8;
     private static final int EVENT_INDEX_EVENT_LOCATION = 9;
@@ -84,19 +121,28 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
 
     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_RELATIONSHIP = 1;
-    private static final int ATTENDEES_INDEX_STATUS = 2;
+    private static final int ATTENDEES_INDEX_ID = 0;
+    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";
 
-    private static final String[] CALENDARS_PROJECTION = new String[] {
-        Calendars._ID,          // 0
-        Calendars.DISPLAY_NAME, // 1
+    static final String[] CALENDARS_PROJECTION = new String[] {
+        Calendars._ID,           // 0
+        Calendars.DISPLAY_NAME,  // 1
+        Calendars.OWNER_ACCOUNT, // 2
     };
-    private static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
-    private static final String CALENDARS_WHERE = Calendars._ID + "=%d";
+    static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
+    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
+
+    static final String CALENDARS_WHERE = Calendars._ID + "=%d";
 
     private static final String[] REMINDERS_PROJECTION = new String[] {
         Reminders._ID,      // 0
@@ -143,8 +189,50 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
     private int mDefaultReminderMinutes;
 
     private DeleteEventHelper mDeleteEventHelper;
+    private EditResponseHelper mEditResponseHelper;
 
     private int mResponseOffset;
+    private int mOriginalAttendeeResponse;
+    private int mAttendeeResponseFromIntent = ATTENDEE_NO_RESPONSE;
+    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;
+    private String mCalendarOwnerAccount = "";
 
     // This is called when one of the "remove reminder" buttons is selected.
     public void onClick(View v) {
@@ -155,6 +243,34 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         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());
+    }
+
+    public void onNothingSelected(AdapterView parent) {
+    }
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -165,6 +281,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         ContentResolver cr = getContentResolver();
         mStartMillis = intent.getLongExtra(EVENT_BEGIN_TIME, 0);
         mEndMillis = intent.getLongExtra(EVENT_END_TIME, 0);
+        mAttendeeResponseFromIntent = intent.getIntExtra(ATTENDEE_STATUS, ATTENDEE_NO_RESPONSE);
         mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null);
         if (initEventCursor()) {
             // The cursor is empty. This can happen if the event was deleted.
@@ -178,7 +295,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         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;
@@ -186,15 +302,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         mCalendarsCursor = managedQuery(uri, CALENDARS_PROJECTION, where, null);
         initCalendarsCursor();
 
-        Resources res = getResources();
-
-        if (mVisibility >= Calendars.CONTRIBUTOR_ACCESS &&
-                mRelationship == Attendees.RELATIONSHIP_ATTENDEE) {
-            setTitle(res.getString(R.string.event_info_title_invite));
-        } else {
-            setTitle(res.getString(R.string.event_info_title));
-        }
-
         // Initialize the reminder values array.
         Resources r = getResources();
         String[] strings = r.getStringArray(R.array.reminder_minutes_values);
@@ -212,7 +319,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
                 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
         mDefaultReminderMinutes = Integer.parseInt(durationString);
 
-        mRemindersContainer = (LinearLayout) findViewById(R.id.reminders_container);
+        mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
 
         // Reminders cursor
         boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
@@ -227,7 +334,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
                     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()) {
@@ -244,7 +351,20 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         updateView();
         updateRemindersVisibility();
 
+        // Setup the + Add Reminder Button
+        View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
+            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
@@ -257,6 +377,18 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         }
         initAttendeesCursor();
         initCalendarsCursor();
+        updateResponse();
+        updateTitle();
+    }
+
+    private void updateTitle() {
+        Resources res = getResources();
+        if (mVisibility >= Calendars.CONTRIBUTOR_ACCESS &&
+                mRelationship == Attendees.RELATIONSHIP_ATTENDEE) {
+            setTitle(res.getString(R.string.event_info_title_invite));
+        } else {
+            setTitle(res.getString(R.string.event_info_title));
+        }
     }
 
     /**
@@ -271,15 +403,59 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         mEventCursor.moveToFirst();
         mVisibility = mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL);
         mEventId = mEventCursor.getInt(EVENT_INDEX_ID);
+        String rRule = mEventCursor.getString(EVENT_INDEX_RRULE);
+        mIsRepeating = (rRule != null);
         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()) {
-                mRelationship = mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+                mAcceptedAttendees.clear();
+                mDeclinedAttendees.clear();
+                mTentativeAttendees.clear();
+
+                do {
+                    int status = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+                    String name = mAttendeesCursor.getString(ATTENDEES_INDEX_NAME);
+                    String email = mAttendeesCursor.getString(ATTENDEES_INDEX_EMAIL);
+                    int relationship = mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP);
+                    if (mRelationship != relationship && mCalendarOwnerAccount.equals(email)) {
+                        mRelationship = relationship;
+                    }
+
+                    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();
             }
         }
+
+        // TODO We shouldn't have to guess whether the current user is the organizer or not
+        if (mVisibility < Calendars.CONTRIBUTOR_ACCESS
+                && mRelationship == Attendees.RELATIONSHIP_ORGANIZER) {
+            mRelationship = Attendees.RELATIONSHIP_ATTENDEE;
+        }
     }
 
     private void initCalendarsCursor() {
@@ -291,11 +467,29 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
     @Override
     public void onPause() {
         super.onPause();
+        if (!isFinishing()) {
+            return;
+        }
         ContentResolver cr = getContentResolver();
         ArrayList<Integer> reminderMinutes = EditEvent.reminderItemsToMinutes(mReminderItems,
                 mReminderValues);
-        EditEvent.saveReminders(cr, mEventId, reminderMinutes, mOriginalMinutes);
-        saveResponse();
+        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();
+        }
     }
 
     @Override
@@ -320,28 +514,33 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
     public boolean onPrepareOptionsMenu(Menu menu) {
         // Cannot add reminders to a shared calendar with only free/busy
         // permissions
-        if (mVisibility >= Calendars.READ_ACCESS && mReminderItems.size() < MAX_REMINDERS) {
-            menu.setGroupVisible(MENU_GROUP_REMINDER, true);
-            menu.setGroupEnabled(MENU_GROUP_REMINDER, true);
-        } else {
-            menu.setGroupVisible(MENU_GROUP_REMINDER, false);
-            menu.setGroupEnabled(MENU_GROUP_REMINDER, false);
-        }
+        boolean canAddReminders =
+                mVisibility >= Calendars.READ_ACCESS && mReminderItems.size() < MAX_REMINDERS;
+               menu.setGroupVisible(MENU_GROUP_REMINDER, canAddReminders);
+               menu.setGroupEnabled(MENU_GROUP_REMINDER, canAddReminders);
+
+        boolean canModifyCalendar = mVisibility >= Calendars.CONTRIBUTOR_ACCESS;
+        boolean canModifyEvent = canModifyCalendar
+                && mRelationship >= Attendees.RELATIONSHIP_ORGANIZER;
+        menu.setGroupVisible(MENU_GROUP_EDIT, canModifyEvent);
+        menu.setGroupEnabled(MENU_GROUP_EDIT, canModifyEvent);
+        menu.setGroupVisible(MENU_GROUP_DELETE, canModifyCalendar);
+        menu.setGroupEnabled(MENU_GROUP_DELETE, canModifyCalendar);
 
-        if (mVisibility >= Calendars.CONTRIBUTOR_ACCESS &&
-                mRelationship >= Attendees.RELATIONSHIP_ORGANIZER) {
-            menu.setGroupVisible(MENU_GROUP_EDIT, true);
-            menu.setGroupEnabled(MENU_GROUP_EDIT, true);
-            menu.setGroupVisible(MENU_GROUP_DELETE, true);
-            menu.setGroupEnabled(MENU_GROUP_DELETE, true);
+        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).
+        if (mDefaultReminderMinutes == 0) {
+            EditEvent.addReminder(this, this, mReminderItems,
+                    mReminderValues, mReminderLabels, 10 /* minutes */);
         } else {
-            menu.setGroupVisible(MENU_GROUP_EDIT, false);
-            menu.setGroupEnabled(MENU_GROUP_EDIT, false);
-            menu.setGroupVisible(MENU_GROUP_DELETE, false);
-            menu.setGroupEnabled(MENU_GROUP_DELETE, false);
+            EditEvent.addReminder(this, this, mReminderItems,
+                    mReminderValues, mReminderLabels, mDefaultReminderMinutes);
         }
-
-        return super.onPrepareOptionsMenu(menu);
+        updateRemindersVisibility();
     }
 
     @Override
@@ -349,16 +548,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         super.onOptionsItemSelected(item);
         switch (item.getItemId()) {
         case MENU_ADD_REMINDER:
-            // TODO: when adding a new reminder, make it different from the
-            // last one in the list (if any).
-            if (mDefaultReminderMinutes == 0) {
-                EditEvent.addReminder(this, this, mReminderItems,
-                        mReminderValues, mReminderLabels, 10 /* minutes */);
-            } else {
-                EditEvent.addReminder(this, this, mReminderItems,
-                        mReminderValues, mReminderLabels, mDefaultReminderMinutes);
-            }
-            updateRemindersVisibility();
+            addReminder();
             break;
         case MENU_EDIT:
             doEdit();
@@ -387,19 +577,104 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         }
     }
 
-    private void saveResponse() {
-        if (mAttendeesCursor == null) {
-            return;
+    /**
+     * 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
+     */
+    private boolean saveResponse(ContentResolver cr) {
+        if (mAttendeesCursor == null || mEventCursor == null) {
+            return false;
         }
         Spinner spinner = (Spinner) findViewById(R.id.response_value);
         int position = spinner.getSelectedItemPosition() - mResponseOffset;
         if (position <= 0) {
-            return;
+            return false;
         }
 
         int status = ATTENDEE_VALUES[position];
-        mAttendeesCursor.updateInt(ATTENDEES_INDEX_STATUS, status);
-        mAttendeesCursor.commitUpdates();
+
+        // If the status has not changed, then don't update the database
+        if (status == mOriginalAttendeeResponse) {
+            return false;
+        }
+
+        // TODO find the right row first
+        long attendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
+        if (!mIsRepeating) {
+            // This is a non-repeating event
+            updateResponse(cr, mEventId, attendeeId, status);
+            return true;
+        }
+
+        // This is a repeating event
+        int whichEvents = mEditResponseHelper.getWhichEvents();
+        switch (whichEvents) {
+            case -1:
+                return false;
+            case UPDATE_SINGLE:
+                createExceptionResponse(cr, mEventId, attendeeId, status);
+                return true;
+            case UPDATE_ALL:
+                updateResponse(cr, mEventId, attendeeId, status);
+                return true;
+            default:
+                Log.e("Calendar", "Unexpected choice for updating invitation response");
+                break;
+        }
+        return false;
+    }
+
+    private void updateResponse(ContentResolver cr, long eventId, long attendeeId, int status) {
+        // Update the "selfAttendeeStatus" field for the event
+        ContentValues values = new ContentValues();
+
+        // Will need to add email when MULTIPLE_ATTENDEES_PER_EVENT supported.
+        values.put(Attendees.ATTENDEE_STATUS, status);
+        values.put(Attendees.EVENT_ID, eventId);
+
+        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.
+        Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, eventId);
+        Cursor cursor = cr.query(uri, EVENT_PROJECTION, null, null, null);
+        if (cursor == null) {
+            return;
+        }
+
+        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);
+            values.put(Events.CALENDAR_ID, calendarId);
+            values.put(Events.DTSTART, mStartMillis);
+            values.put(Events.DTEND, mEndMillis);
+            values.put(Events.ORIGINAL_EVENT, syncId);
+            values.put(Events.ORIGINAL_INSTANCE_TIME, mStartMillis);
+            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 {
+            cursor.close();
+        }
     }
 
     private int findResponseIndexFor(int response) {
@@ -431,7 +706,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
             return;
         }
         Resources res = getResources();
-        ContentResolver cr = getContentResolver();
 
         String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
         if (eventName == null || eventName.length() == 0) {
@@ -444,10 +718,16 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         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(mColor);
 
-        ImageView stripe = (ImageView) findViewById(R.id.vertical_stripe);
-        stripe.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+        TextView title = (TextView) findViewById(R.id.title);
+        title.setTextColor(mColor);
+
+        View divider = findViewById(R.id.divider);
+        divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
 
         // What
         if (eventName != null) {
@@ -475,7 +755,15 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
             localTimezone = Time.TIMEZONE_UTC;
         }
         if (eventTimezone != null && !localTimezone.equals(eventTimezone) && !allDay) {
-            setTextCommon(R.id.timezone, localTimezone);
+            String displayName;
+            TimeZone tz = TimeZone.getTimeZone(localTimezone);
+            if (tz == null || tz.getID().equals("GMT")) {
+                displayName = localTimezone;
+            } else {
+                displayName = tz.getDisplayName();
+            }
+
+            setTextCommon(R.id.timezone, displayName);
         } else {
             setVisibilityCommon(R.id.timezone_container, View.GONE);
         }
@@ -500,7 +788,22 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         if (location == null || location.length() == 0) {
             setVisibilityCommon(R.id.where, View.GONE);
         } else {
-            setTextCommon(R.id.where, location);
+            final TextView textView = (TextView) findViewById(R.id.where);
+            if (textView != null) {
+                    textView.setAutoLinkMask(0);
+                    textView.setText(location);
+                    Linkify.addLinks(textView, mWildcardPattern, "geo:0,0?q=");
+                    textView.setOnTouchListener(new OnTouchListener() {
+                        public boolean onTouch(View v, MotionEvent event) {
+                            try {
+                                return v.onTouchEvent(event);
+                            } catch (ActivityNotFoundException e) {
+                                // ignore
+                                return true;
+                            }
+                        }
+                    });
+            }
         }
 
         // Description
@@ -515,12 +818,129 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
             mCalendarsCursor.moveToFirst();
             String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
             setTextCommon(R.id.calendar, calendarName);
+            mCalendarOwnerAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
         } 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() {
@@ -534,9 +954,10 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
 
         Spinner spinner = (Spinner) findViewById(R.id.response_value);
 
-        int response = ATTENDEE_NO_RESPONSE;
+        mOriginalAttendeeResponse = ATTENDEE_NO_RESPONSE;
         if (mAttendeesCursor != null) {
-            response = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+            // TODO find the right row first
+            mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
         }
         mResponseOffset = 0;
 
@@ -545,9 +966,9 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
          * Switch the entries to a set of entries without the
          * no response option.
          */
-        if ((response != Attendees.ATTENDEE_STATUS_INVITED)
-                && (response != ATTENDEE_NO_RESPONSE)
-                && (response != Attendees.ATTENDEE_STATUS_NONE)) {
+        if ((mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_INVITED)
+                && (mOriginalAttendeeResponse != ATTENDEE_NO_RESPONSE)
+                && (mOriginalAttendeeResponse != Attendees.ATTENDEE_STATUS_NONE)) {
             CharSequence[] entries;
             entries = getResources().getTextArray(R.array.response_labels2);
             mResponseOffset = -1;
@@ -558,8 +979,14 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
             spinner.setAdapter(adapter);
         }
 
-        int index = findResponseIndexFor(response);
+        int index;
+        if (mAttendeeResponseFromIntent != ATTENDEE_NO_RESPONSE) {
+            index = findResponseIndexFor(mAttendeeResponseFromIntent);
+        } else {
+            index = findResponseIndexFor(mOriginalAttendeeResponse);
+        }
         spinner.setSelection(index + mResponseOffset);
+        spinner.setOnItemSelectedListener(this);
     }
 
     private void setTextCommon(int id, CharSequence text) {
@@ -576,4 +1003,31 @@ public class EventInfoActivity extends Activity implements View.OnClickListener
         }
         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
+        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);
+    }
 }