OSDN Git Service

b/2531257 More work on cleaning up owner account for dupes
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / EventInfoActivity.java
index ff0e97c..8f4b4cc 100644 (file)
@@ -21,6 +21,7 @@ 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;
@@ -37,19 +38,21 @@ 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.ContactsContract;
 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.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.QuickContact;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.text.format.DateUtils;
 import android.text.format.Time;
@@ -60,13 +63,15 @@ 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.QuickContactBadge;
 import android.widget.Spinner;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -74,11 +79,15 @@ 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,
         AdapterView.OnItemSelectedListener {
-    private static final String TAG = "EventInfoActivity";
+    public static final boolean DEBUG = false;
+
+    public static final String TAG = "EventInfoActivity";
+
     private static final int MAX_REMINDERS = 5;
 
     /**
@@ -102,6 +111,11 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         Events.HAS_ALARM,            // 10
         Events.ACCESS_LEVEL,         // 11
         Events.COLOR,                // 12
+        Events.HAS_ATTENDEE_DATA,    // 13
+        Events.GUESTS_CAN_MODIFY,    // 14
+        // TODO Events.GUESTS_CAN_INVITE_OTHERS has not been implemented in calendar provider
+        Events.GUESTS_CAN_INVITE_OTHERS, // 15
+        Events.ORGANIZER,            // 16
     };
     private static final int EVENT_INDEX_ID = 0;
     private static final int EVENT_INDEX_TITLE = 1;
@@ -115,6 +129,10 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     private static final int EVENT_INDEX_HAS_ALARM = 10;
     private static final int EVENT_INDEX_ACCESS_LEVEL = 11;
     private static final int EVENT_INDEX_COLOR = 12;
+    private static final int EVENT_INDEX_HAS_ATTENDEE_DATA = 13;
+    private static final int EVENT_INDEX_GUESTS_CAN_MODIFY = 14;
+    private static final int EVENT_INDEX_CAN_INVITE_OTHERS = 15;
+    private static final int EVENT_INDEX_ORGANIZER = 16;
 
     private static final String[] ATTENDEES_PROJECTION = new String[] {
         Attendees._ID,                      // 0
@@ -131,12 +149,21 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
     private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=%d";
 
+    private static final String ATTENDEES_SORT_ORDER = Attendees.ATTENDEE_NAME + " ASC, "
+            + Attendees.ATTENDEE_EMAIL + " ASC";
+
     static final String[] CALENDARS_PROJECTION = new String[] {
-        Calendars._ID,          // 0
-        Calendars.DISPLAY_NAME, // 1
+        Calendars._ID,           // 0
+        Calendars.DISPLAY_NAME,  // 1
+        Calendars.OWNER_ACCOUNT, // 2
+        Calendars.ORGANIZER_CAN_RESPOND // 3
     };
     static final int CALENDARS_INDEX_DISPLAY_NAME = 1;
+    static final int CALENDARS_INDEX_OWNER_ACCOUNT = 2;
+    static final int CALENDARS_INDEX_OWNER_CAN_RESPOND = 3;
+
     static final String CALENDARS_WHERE = Calendars._ID + "=%d";
+    static final String CALENDARS_DUPLICATE_NAME_WHERE = Calendars.DISPLAY_NAME + "=?";
 
     private static final String[] REMINDERS_PROJECTION = new String[] {
         Reminders._ID,      // 0
@@ -146,6 +173,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     private static final String REMINDERS_WHERE = Reminders.EVENT_ID + "=%d AND (" +
             Reminders.METHOD + "=" + Reminders.METHOD_ALERT + " OR " + Reminders.METHOD + "=" +
             Reminders.METHOD_DEFAULT + ")";
+    private static final String REMINDERS_SORT = Reminders.MINUTES;
 
     private static final int MENU_GROUP_REMINDER = 1;
     private static final int MENU_GROUP_EDIT = 2;
@@ -155,6 +183,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     private static final int MENU_EDIT = 2;
     private static final int MENU_DELETE = 3;
 
+    private static final int ATTENDEE_ID_NONE = -1;
     private static final int ATTENDEE_NO_RESPONSE = -1;
     private static final int[] ATTENDEE_VALUES = {
             ATTENDEE_NO_RESPONSE,
@@ -164,6 +193,8 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     };
 
     private LinearLayout mRemindersContainer;
+    private LinearLayout mOrganizerContainer;
+    private TextView mOrganizerView;
 
     private Uri mUri;
     private long mEventId;
@@ -173,14 +204,24 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
     private long mStartMillis;
     private long mEndMillis;
-    private int mVisibility = Calendars.NO_ACCESS;
-    private int mRelationship = Attendees.RELATIONSHIP_ORGANIZER;
+
+    private boolean mHasAttendeeData;
+    private boolean mIsOrganizer;
+    private long mCalendarOwnerAttendeeId = ATTENDEE_ID_NONE;
+    private boolean mOrganizerCanRespond;
+    private String mCalendarOwnerAccount;
+    private boolean mCanModifyCalendar;
+    private boolean mIsBusyFreeCalendar;
+    private boolean mCanModifyEvent;
+    private int mNumOfAttendees;
+    private String mOrganizer;
 
     private ArrayList<Integer> mOriginalMinutes = new ArrayList<Integer>();
     private ArrayList<LinearLayout> mReminderItems = new ArrayList<LinearLayout>(0);
     private ArrayList<Integer> mReminderValues;
     private ArrayList<String> mReminderLabels;
     private int mDefaultReminderMinutes;
+    private boolean mOriginalHasAlarm;
 
     private DeleteEventHelper mDeleteEventHelper;
     private EditResponseHelper mEditResponseHelper;
@@ -189,42 +230,40 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     private int mOriginalAttendeeResponse;
     private int mAttendeeResponseFromIntent = ATTENDEE_NO_RESPONSE;
     private boolean mIsRepeating;
+    private boolean mIsDuplicateName;
 
     private Pattern mWildcardPattern = Pattern.compile("^.*$");
     private LayoutInflater mLayoutInflater;
+    private LinearLayout mReminderAdder;
 
+    // TODO This can be removed when the contacts content provider doesn't return duplicates
+    private int mUpdateCounts;
     private static class ViewHolder {
-        ImageView avatar;
+        QuickContactBadge badge;
         ImageView presence;
+        int updateCounts;
     }
-    private HashMap<String, ViewHolder> mPresenceStatuses = new HashMap<String, ViewHolder>();
+    private HashMap<String, ViewHolder> mViewHolders = new HashMap<String, ViewHolder>();
     private PresenceQueryHandler mPresenceQueryHandler;
 
-    static final String[] PEOPLE_PROJECTION = new String[] {
-        People._ID,
-    };
+    private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI;
+
+    int PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0;
+    int PRESENCE_PROJECTION_PRESENCE_INDEX = 1;
+    int PRESENCE_PROJECTION_EMAIL_INDEX = 2;
+    int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3;
 
-    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
+        Email.CONTACT_ID,           // 0
+        Email.CONTACT_PRESENCE,     // 1
+        Email.DATA,                 // 2
+        Email.PHOTO_ID,             // 3
     };
 
     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);
-        }
-    };
+    ArrayList<Attendee> mNoResponseAttendees = new ArrayList<Attendee>();
     private int mColor;
 
     // This is called when one of the "remove reminder" buttons is selected.
@@ -236,7 +275,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         updateRemindersVisibility();
     }
 
-    public void onItemSelected(AdapterView parent, View v, int position, long id) {
+    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) {
@@ -261,7 +300,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         mEditResponseHelper.showDialog(mEditResponseHelper.getWhichEvents());
     }
 
-    public void onNothingSelected(AdapterView parent) {
+    public void onNothingSelected(AdapterView<?> parent) {
     }
 
     @Override
@@ -275,7 +314,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         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);
+        mEventCursor = managedQuery(mUri, EVENT_PROJECTION, null, null, null);
         if (initEventCursor()) {
             // The cursor is empty. This can happen if the event was deleted.
             finish();
@@ -283,26 +322,46 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         }
 
         setContentView(R.layout.event_info_activity);
-
-        // Attendees cursor
-        Uri uri = Attendees.CONTENT_URI;
-        String where = String.format(ATTENDEES_WHERE, mEventId);
-        mAttendeesCursor = managedQuery(uri, ATTENDEES_PROJECTION, where, null);
+        mPresenceQueryHandler = new PresenceQueryHandler(this, cr);
+        mLayoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mRemindersContainer = (LinearLayout) findViewById(R.id.reminders_container);
+        mOrganizerContainer = (LinearLayout) findViewById(R.id.organizer_container);
+        mOrganizerView = (TextView) findViewById(R.id.organizer);
 
         // Calendars cursor
-        uri = Calendars.CONTENT_URI;
-        where = String.format(CALENDARS_WHERE, mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID));
-        mCalendarsCursor = managedQuery(uri, CALENDARS_PROJECTION, where, null);
-        initCalendarsCursor();
-
-        Resources res = getResources();
+        Uri uri = Calendars.CONTENT_URI;
+        String where = String.format(CALENDARS_WHERE, mEventCursor.getLong(EVENT_INDEX_CALENDAR_ID));
+        mCalendarsCursor = managedQuery(uri, CALENDARS_PROJECTION, where, null, null);
+        mCalendarOwnerAccount = "";
+        if (mCalendarsCursor != null) {
+            mCalendarsCursor.moveToFirst();
+            mCalendarOwnerAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
+            mOrganizerCanRespond = mCalendarsCursor.getInt(CALENDARS_INDEX_OWNER_CAN_RESPOND) != 0;
 
-        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));
+            String displayName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
+            mIsDuplicateName = isDuplicateName(displayName);
         }
+        String eventOrganizer = mEventCursor.getString(EVENT_INDEX_ORGANIZER);
+        mIsOrganizer = mCalendarOwnerAccount.equalsIgnoreCase(eventOrganizer);
+        mHasAttendeeData = mEventCursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
+
+        updateView();
+
+        // Attendees cursor
+        uri = Attendees.CONTENT_URI;
+        where = String.format(ATTENDEES_WHERE, mEventId);
+        mAttendeesCursor = managedQuery(uri, ATTENDEES_PROJECTION, where, null,
+                ATTENDEES_SORT_ORDER);
+        initAttendeesCursor();
+
+        mOrganizer = eventOrganizer;
+        mCanModifyCalendar =
+                mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) >= Calendars.CONTRIBUTOR_ACCESS;
+        mIsBusyFreeCalendar =
+                mEventCursor.getInt(EVENT_INDEX_ACCESS_LEVEL) == Calendars.FREEBUSY_ACCESS;
+
+        mCanModifyEvent = mCanModifyCalendar
+                && (mIsOrganizer || (mEventCursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0));
 
         // Initialize the reminder values array.
         Resources r = getResources();
@@ -316,19 +375,18 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         String[] labels = r.getStringArray(R.array.reminder_minutes_labels);
         mReminderLabels = new ArrayList<String>(Arrays.asList(labels));
 
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+        SharedPreferences prefs = CalendarPreferenceActivity.getSharedPreferences(this);
         String durationString =
                 prefs.getString(CalendarPreferenceActivity.KEY_DEFAULT_REMINDER, "0");
         mDefaultReminderMinutes = Integer.parseInt(durationString);
 
-        mRemindersContainer = (LinearLayout) findViewById(R.id.reminder_items_container);
-
         // Reminders cursor
         boolean hasAlarm = mEventCursor.getInt(EVENT_INDEX_HAS_ALARM) != 0;
         if (hasAlarm) {
             uri = Reminders.CONTENT_URI;
             where = String.format(REMINDERS_WHERE, mEventId);
-            Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null, null);
+            Cursor reminderCursor = cr.query(uri, REMINDERS_PROJECTION, where, null,
+                    REMINDERS_SORT);
             try {
                 // First pass: collect all the custom reminder minutes (e.g.,
                 // a reminder of 8 minutes) into a global list.
@@ -349,9 +407,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
                 reminderCursor.close();
             }
         }
-
-        updateView();
-        updateRemindersVisibility();
+        mOriginalHasAlarm = hasAlarm;
 
         // Setup the + Add Reminder Button
         View.OnClickListener addReminderOnClickListener = new View.OnClickListener() {
@@ -359,14 +415,14 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
                 addReminder();
             }
         };
-        ImageButton reminderRemoveButton = (ImageButton) findViewById(R.id.reminder_add);
-        reminderRemoveButton.setOnClickListener(addReminderOnClickListener);
+        ImageButton reminderAddButton = (ImageButton) findViewById(R.id.reminder_add);
+        reminderAddButton.setOnClickListener(addReminderOnClickListener);
+
+        mReminderAdder = (LinearLayout) findViewById(R.id.reminder_adder);
+        updateRemindersVisibility();
 
         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
@@ -377,9 +433,31 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             finish();
             return;
         }
-        initAttendeesCursor();
         initCalendarsCursor();
         updateResponse();
+        updateTitle();
+    }
+
+    private void updateTitle() {
+        Resources res = getResources();
+        if (mCanModifyCalendar && !mIsOrganizer) {
+            setTitle(res.getString(R.string.event_info_title_invite));
+        } else {
+            setTitle(res.getString(R.string.event_info_title));
+        }
+    }
+
+    boolean isDuplicateName(String displayName) {
+        Cursor dupNameCursor = managedQuery(Calendars.CONTENT_URI, CALENDARS_PROJECTION,
+                CALENDARS_DUPLICATE_NAME_WHERE, new String[] {displayName}, null);
+        boolean isDuplicateName = false;
+        if(dupNameCursor != null) {
+            if (dupNameCursor.getCount() > 1) {
+                isDuplicateName = true;
+            }
+            dupNameCursor.close();
+        }
+        return isDuplicateName;
     }
 
     /**
@@ -392,7 +470,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             return true;
         }
         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);
@@ -409,32 +486,55 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         }
     }
 
+    @SuppressWarnings("fallthrough")
     private void initAttendeesCursor() {
+        mOriginalAttendeeResponse = ATTENDEE_NO_RESPONSE;
+        mCalendarOwnerAttendeeId = ATTENDEE_ID_NONE;
+        mNumOfAttendees = 0;
         if (mAttendeesCursor != null) {
+            mNumOfAttendees = mAttendeesCursor.getCount();
             if (mAttendeesCursor.moveToFirst()) {
                 mAcceptedAttendees.clear();
                 mDeclinedAttendees.clear();
                 mTentativeAttendees.clear();
+                mNoResponseAttendees.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));
+
+                    if (mAttendeesCursor.getInt(ATTENDEES_INDEX_RELATIONSHIP) ==
+                            Attendees.RELATIONSHIP_ORGANIZER) {
+                        // Overwrites the one from Event table if available
+                        if (name != null && name.length() > 0) {
+                            mOrganizer = name;
+                        } else if (email != null && email.length() > 0) {
+                            mOrganizer = email;
+                        }
+                    }
+
+                    if (mCalendarOwnerAttendeeId == ATTENDEE_ID_NONE &&
+                            mCalendarOwnerAccount.equalsIgnoreCase(email)) {
+                        mCalendarOwnerAttendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
+                        mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
+                    } else {
+                        // Don't show your own status in the list because:
+                        //  1) it doesn't make sense for event without other guests.
+                        //  2) there's a spinner for that for events with guests.
+                        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;
+                            case Attendees.ATTENDEE_STATUS_NONE:
+                                mNoResponseAttendees.add(new Attendee(name, email));
+                                // Fallthrough so that no response is a subset of tentative
+                            default:
+                                mTentativeAttendees.add(new Attendee(name, email));
+                        }
                     }
                 } while (mAttendeesCursor.moveToNext());
                 mAttendeesCursor.moveToFirst();
@@ -442,6 +542,15 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
                 updateAttendees();
             }
         }
+        // only show the organizer if we're not the organizer and if
+        // we have attendee data (might have been removed by the server
+        // for events with a lot of attendees).
+        if (!mIsOrganizer && mHasAttendeeData) {
+            mOrganizerContainer.setVisibility(View.VISIBLE);
+            mOrganizerView.setText(mOrganizer);
+        } else {
+            mOrganizerContainer.setVisibility(View.GONE);
+        }
     }
 
     private void initCalendarsCursor() {
@@ -463,13 +572,21 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         boolean changed = EditEvent.saveReminders(ops, mEventId, reminderMinutes, mOriginalMinutes,
                 false /* no force save */);
         try {
+            // TODO Run this in a background process.
             cr.applyBatch(Calendars.CONTENT_URI.getAuthority(), ops);
+            // Update the "hasAlarm" field for the event
+            Uri uri = ContentUris.withAppendedId(Events.CONTENT_URI, mEventId);
+            int len = reminderMinutes.size();
+            boolean hasAlarm = len > 0;
+            if (hasAlarm != mOriginalHasAlarm) {
+                ContentValues values = new ContentValues();
+                values.put(Events.HAS_ALARM, hasAlarm ? 1 : 0);
+                cr.update(uri, values, null, null);
+            }
         } catch (RemoteException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            Log.w(TAG, "Ignoring exception: ", e);
         } catch (OperationApplicationException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            Log.w(TAG, "Ignoring exception: ", e);
         }
 
         changed |= saveResponse(cr);
@@ -498,32 +615,22 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
     @Override
     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 = canAddReminders();
+        menu.setGroupVisible(MENU_GROUP_REMINDER, canAddReminders);
+        menu.setGroupEnabled(MENU_GROUP_REMINDER, canAddReminders);
 
-        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);
-        } 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);
-        }
+        menu.setGroupVisible(MENU_GROUP_EDIT, mCanModifyEvent);
+        menu.setGroupEnabled(MENU_GROUP_EDIT, mCanModifyEvent);
+        menu.setGroupVisible(MENU_GROUP_DELETE, mCanModifyCalendar);
+        menu.setGroupEnabled(MENU_GROUP_DELETE, mCanModifyCalendar);
 
         return super.onPrepareOptionsMenu(menu);
     }
 
+    private boolean canAddReminders() {
+        return !mIsBusyFreeCalendar && mReminderItems.size() < MAX_REMINDERS;
+    }
+
     private void addReminder() {
         // TODO: when adding a new reminder, make it different from the
         // last one in the list (if any).
@@ -564,10 +671,11 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     }
 
     private void updateRemindersVisibility() {
-        if (mReminderItems.size() == 0) {
+        if (mIsBusyFreeCalendar) {
             mRemindersContainer.setVisibility(View.GONE);
         } else {
             mRemindersContainer.setVisibility(View.VISIBLE);
+            mReminderAdder.setVisibility(canAddReminders() ? View.VISIBLE : View.GONE);
         }
     }
 
@@ -595,10 +703,14 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             return false;
         }
 
-        long attendeeId = mAttendeesCursor.getInt(ATTENDEES_INDEX_ID);
+        // If we never got an owner attendee id we can't set the status
+        if (mCalendarOwnerAttendeeId == ATTENDEE_ID_NONE) {
+            return false;
+        }
+
         if (!mIsRepeating) {
             // This is a non-repeating event
-            updateResponse(cr, mEventId, attendeeId, status);
+            updateResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
             return true;
         }
 
@@ -608,23 +720,26 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             case -1:
                 return false;
             case UPDATE_SINGLE:
-                createExceptionResponse(cr, mEventId, attendeeId, status);
+                createExceptionResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
                 return true;
             case UPDATE_ALL:
-                updateResponse(cr, mEventId, attendeeId, status);
+                updateResponse(cr, mEventId, mCalendarOwnerAttendeeId, status);
                 return true;
             default:
-                Log.e("Calendar", "Unexpected choice for updating invitation response");
+                Log.e(TAG, "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
+        // Update the attendee status in the attendees table.  the provider
+        // takes care of updating the self attendance status.
         ContentValues values = new ContentValues();
 
-        // Will need to add email when MULTIPLE_ATTENDEES_PER_EVENT supported.
+        if (!TextUtils.isEmpty(mCalendarOwnerAccount)) {
+            values.put(Attendees.ATTENDEE_EMAIL, mCalendarOwnerAccount);
+        }
         values.put(Attendees.ATTENDEE_STATUS, status);
         values.put(Attendees.EVENT_ID, eventId);
 
@@ -640,9 +755,12 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         if (cursor == null) {
             return;
         }
+        if(!cursor.moveToFirst()) {
+            cursor.close();
+            return;
+        }
 
         try {
-            cursor.moveToFirst();
             ContentValues values = new ContentValues();
 
             String title = cursor.getString(EVENT_INDEX_TITLE);
@@ -698,10 +816,10 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         if (mEventCursor == null) {
             return;
         }
-        Resources res = getResources();
 
         String eventName = mEventCursor.getString(EVENT_INDEX_TITLE);
         if (eventName == null || eventName.length() == 0) {
+            Resources res = getResources();
             eventName = res.getString(R.string.no_title_label);
         }
 
@@ -748,7 +866,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);
         }
@@ -763,7 +889,8 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             }
             date.set(mStartMillis);
             eventRecurrence.setStartDate(date);
-            String repeatString = eventRecurrence.getRepeatString();
+            String repeatString = EventRecurrenceFormatter.getRepeatString(getResources(),
+                    eventRecurrence);
             setTextCommon(R.id.repeat, repeatString);
         } else {
             setVisibilityCommon(R.id.repeat_container, View.GONE);
@@ -773,11 +900,21 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         if (location == null || location.length() == 0) {
             setVisibilityCommon(R.id.where, View.GONE);
         } else {
-            TextView textView = (TextView) findViewById(R.id.where);
+            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;
+                            }
+                        }
+                    });
             }
         }
 
@@ -790,8 +927,16 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
         // Calendar
         if (mCalendarsCursor != null) {
-            mCalendarsCursor.moveToFirst();
             String calendarName = mCalendarsCursor.getString(CALENDARS_INDEX_DISPLAY_NAME);
+            String ownerAccount = mCalendarsCursor.getString(CALENDARS_INDEX_OWNER_ACCOUNT);
+            if (mIsDuplicateName && !calendarName.equalsIgnoreCase(ownerAccount)) {
+                Resources res = getResources();
+                TextView ownerText = (TextView) findViewById(R.id.owner);
+                ownerText.setText(ownerAccount);
+                ownerText.setTextColor(res.getColor(R.color.calendar_owner_text_color));
+            } else {
+                setVisibilityCommon(R.id.owner, View.GONE);
+            }
             setTextCommon(R.id.calendar, calendarName);
         } else {
             setVisibilityCommon(R.id.calendar_container, View.GONE);
@@ -799,13 +944,22 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
     }
 
     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]);
+        ++mUpdateCounts;
+        if(mAcceptedAttendees.size() == 0 && mDeclinedAttendees.size() == 0 &&
+                mTentativeAttendees.size() == mNoResponseAttendees.size()) {
+            // If all guests have no response just list them as guests,
+            CharSequence guestsLabel = getResources().getText(R.string.attendees_label);
+            addAttendeesToLayout(mNoResponseAttendees, attendeesLayout, guestsLabel);
+        } else {
+            // If we have any responses then divide them up by response
+            CharSequence[] entries;
+            entries = getResources().getTextArray(R.array.response_labels2);
+            addAttendeesToLayout(mAcceptedAttendees, attendeesLayout, entries[0]);
+            addAttendeesToLayout(mDeclinedAttendees, attendeesLayout, entries[2]);
+            addAttendeesToLayout(mTentativeAttendees, attendeesLayout, entries[1]);
+        }
     }
 
     private void addAttendeesToLayout(ArrayList<Attendee> attendees, LinearLayout attendeeList,
@@ -817,7 +971,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         ContentResolver cr = getContentResolver();
         // Yes/No/Maybe Title
         View titleView = mLayoutInflater.inflate(R.layout.contact_item, null);
-        titleView.findViewById(R.id.avatar).setVisibility(View.GONE);
+        titleView.findViewById(R.id.badge).setVisibility(View.GONE);
         View divider = titleView.findViewById(R.id.separator);
         divider.getBackground().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
 
@@ -828,7 +982,7 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
         // Attendees
         int numOfAttendees = attendees.size();
-        StringBuilder selection = new StringBuilder(Contacts.ContactMethods.DATA + " IN (");
+        StringBuilder selection = new StringBuilder(Email.DATA + " IN (");
         String[] selectionArgs = new String[numOfAttendees];
 
         for (int i = 0; i < numOfAttendees; ++i) {
@@ -836,7 +990,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             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);
@@ -851,9 +1004,10 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             tv.setText(name);
 
             ViewHolder vh = new ViewHolder();
-            vh.avatar = (ImageView) v.findViewById(R.id.avatar);
+            vh.badge = (QuickContactBadge) v.findViewById(R.id.badge);
+            vh.badge.assignContactFromEmail(attendee.mEmail, true);
             vh.presence = (ImageView) v.findViewById(R.id.presence);
-            mPresenceStatuses.put(attendee.mEmail, vh);
+            mViewHolders.put(attendee.mEmail, vh);
 
             if (i == 0) {
                 selection.append('?');
@@ -865,8 +1019,8 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
         }
         selection.append(')');
 
-        mPresenceQueryHandler.startQuery(0, attendees, CONTACT_PRESENCE_URI, PRESENCE_PROJECTION,
-                selection.toString(), selectionArgs, null);
+        mPresenceQueryHandler.startQuery(mUpdateCounts, attendees, CONTACT_DATA_WITH_PRESENCE_URI,
+                PRESENCE_PROJECTION, selection.toString(), selectionArgs, null);
     }
 
     private class PresenceQueryHandler extends AsyncQueryHandler {
@@ -879,49 +1033,64 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
             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);
+        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
+            if (cursor == null) {
+                if (DEBUG) {
+                    Log.e(TAG, "onQueryComplete: cursor == null");
                 }
+                return;
             }
 
-            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);
-                        }
+            try {
+                cursor.moveToPosition(-1);
+                while (cursor.moveToNext()) {
+                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
+                    int contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
+                    ViewHolder vh = mViewHolders.get(email);
+                    int photoId = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
+                    if (DEBUG) {
+                        Log.e(TAG, "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
+                                + " Email: " + email);
+                    }
+                    if (vh == null) {
+                        continue;
+                    }
+                    ImageView presenceView = vh.presence;
+                    if (presenceView != null) {
+                        int status = cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX);
+                        presenceView.setImageResource(Presence.getPresenceIconResourceId(status));
+                        presenceView.setVisibility(View.VISIBLE);
+                    }
+
+                    if (photoId > 0 && vh.updateCounts < queryIndex) {
+                        vh.updateCounts = queryIndex;
+                        Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+                        // TODO, modify to batch queries together
+                        ContactsAsyncHelper.updateImageViewWithContactPhotoAsync(mContext, vh.badge,
+                                personUri, R.drawable.ic_contact_picture);
                     }
-                    personCursor.close();
                 }
+            } finally {
+                cursor.close();
             }
         }
     }
 
     void updateResponse() {
-        if (mVisibility < Calendars.CONTRIBUTOR_ACCESS ||
-                // TODO Temp fix to get past FAST tests.
-                // Remove this once content provider provides user relationship info via event table
-                (false && mRelationship != Attendees.RELATIONSHIP_ATTENDEE)) {
+        // we only let the user accept/reject/etc. a meeting if:
+        // a) you can edit the event's containing calendar AND
+        // b) you're not the organizer and only attendee AND
+        // c) organizerCanRespond is enabled for the calendar
+        // (if the attendee data has been hidden, the visible number of attendees
+        // will be 1 -- the calendar owner's).
+        // (there are more cases involved to be 100% accurate, such as
+        // paying attention to whether or not an attendee status was
+        // included in the feed, but we're currently omitting those corner cases
+        // for simplicity).
+        if (!mCanModifyCalendar || (mHasAttendeeData && mIsOrganizer && mNumOfAttendees <= 1) ||
+                (mIsOrganizer && !mOrganizerCanRespond)) {
             setVisibilityCommon(R.id.response_container, View.GONE);
             return;
         }
@@ -930,10 +1099,6 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
 
         Spinner spinner = (Spinner) findViewById(R.id.response_value);
 
-        mOriginalAttendeeResponse = ATTENDEE_NO_RESPONSE;
-        if (mAttendeesCursor != null) {
-            mOriginalAttendeeResponse = mAttendeesCursor.getInt(ATTENDEES_INDEX_STATUS);
-        }
         mResponseOffset = 0;
 
         /* If the user has previously responded to this event
@@ -985,25 +1150,32 @@ public class EventInfoActivity extends Activity implements View.OnClickListener,
      * 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);
+        // First perform lookup query to find existing contact
+        final ContentResolver resolver = getContentResolver();
+        final String address = attendee.mEmail;
+        final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
+                Uri.encode(address));
+        final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
+
+        if (lookupUri != null) {
+            // Found matching contact, trigger QuickContact
+            QuickContact.showQuickContact(this, rect, lookupUri, QuickContact.MODE_MEDIUM, null);
+        } else {
+            // No matching contact, ask user to create one
+            final Uri mailUri = Uri.fromParts("mailto", address, null);
+            final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, mailUri);
+
+            // Pass along full E-mail string for possible create dialog
+            Rfc822Token sender = new Rfc822Token(attendee.mName, attendee.mEmail, null);
+            intent.putExtra(Intents.EXTRA_CREATE_DESCRIPTION, sender.toString());
+
+            // Only provide personal name hint if we have one
+            final String senderPersonal = attendee.mName;
+            if (!TextUtils.isEmpty(senderPersonal)) {
+                intent.putExtra(Intents.Insert.NAME, senderPersonal);
+            }
 
-        // 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(intent);
         }
-
-        startActivity(contactIntent);
     }
 }