Replace ListView for Attendees with LinearLayout

Stop using ListView + Adapter implementation and use LinearLayout.

- rename AttendeesAdapter to AttendeesView.
- let attendee list maintained by LinearLayout (using tag),
  instead of manually having it with an array.
- remove unnecessary logic which has existed in the adapter.

TODO:
We still see some issue similar to 3101699.

Change-Id: Ic19343795a6f9d16632f7338d4d087e4fb44ddc0
diff --git a/res/layout/contact_item.xml b/res/layout/contact_item.xml
index d8b840c..49d561b 100644
--- a/res/layout/contact_item.xml
+++ b/res/layout/contact_item.xml
@@ -4,9 +4,9 @@
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
-  
+
           http://www.apache.org/licenses/LICENSE-2.0
-  
+
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,11 +14,11 @@
      limitations under the License.
 -->
 
-<RelativeLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:paddingLeft="0dip"
-    android:paddingRight="0dip"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
     android:minHeight="48dip">
 
     <QuickContactBadge
@@ -27,50 +27,40 @@
         android:paddingRight="3dip"
         android:paddingTop="3dip"
         android:paddingBottom="6dip"
-        android:layout_above="@+id/separator"
         android:layout_marginLeft="2dip"
         android:layout_marginRight="14dip"
         android:layout_marginTop="4dip"
         android:layout_marginBottom="3dip"
-        android:layout_alignParentLeft="true"
-        android:layout_alignParentTop="true"
         android:src="@drawable/ic_contact_picture"
         style="?android:attr/quickContactBadgeStyleWindowMedium" />
 
-    <ImageView
-        android:id="@+id/presence"
-        android:scaleType="fitXY"
-        android:visibility="gone"
-        android:layout_toRightOf="@id/badge"
-        android:layout_centerVertical="true"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content" />
-
-    <ImageButton android:id="@+id/contact_remove"
-        style="@style/MinusButton"
-        android:visibility="gone"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true" />
-
     <TextView
         android:id="@+id/name"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:inputType="none"
-        android:paddingLeft="2dip"
-        android:layout_centerVertical="true"
-        android:layout_toRightOf="@id/presence"
-        android:layout_toLeftOf="@id/contact_remove"
-        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        style="@style/TextAppearance.EditEvent_Label"/>
+        android:layout_width="0dip"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:gravity="center_vertical"
+        android:paddingRight="3dip"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        style="@style/TextAppearance.EditEvent_Label" />
 
-    <View
-        android:id="@+id/separator"
-        android:layout_width="match_parent"
-        android:layout_height="1px"
-        android:layout_alignParentBottom="true"
-        android:background="@android:drawable/divider_horizontal_bright" />
+    <ImageView
+        android:id="@+id/presence"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:paddingRight="3dip"
+        android:scaleType="fitXY"
+        android:visibility="gone" />
 
-</RelativeLayout>
+    <ImageButton
+        android:id="@+id/contact_remove"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:paddingRight="3dip"
+        android:visibility="gone"
+        style="@style/MinusButton" />
+
+</LinearLayout>
diff --git a/res/layout/edit_event.xml b/res/layout/edit_event.xml
index 151c842..3768ff3 100644
--- a/res/layout/edit_event.xml
+++ b/res/layout/edit_event.xml
@@ -302,10 +302,10 @@
                         style="@style/TextAppearance.EditEvent_Label" />
 
                     <!-- GUEST LIST -->
-                    <ListView
+                    <com.android.calendar.event.AttendeesView
                         android:id="@+id/attendee_list"
                         android:orientation="vertical">
-                    </ListView>
+                    </com.android.calendar.event.AttendeesView>
                 </TableRow>
             </TableLayout>
         </LinearLayout>
diff --git a/src/com/android/calendar/event/AttendeesAdapter.java b/src/com/android/calendar/event/AttendeesAdapter.java
deleted file mode 100644
index 55ba9cb..0000000
--- a/src/com/android/calendar/event/AttendeesAdapter.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.calendar.event;
-
-import com.android.calendar.CalendarEventModel.Attendee;
-import com.android.calendar.ContactsAsyncHelper;
-import com.android.calendar.R;
-import com.android.calendar.event.EditEventHelper.AttendeeItem;
-import com.android.common.Rfc822Validator;
-
-import android.content.AsyncQueryHandler;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.provider.Calendar.Attendees;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-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.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.QuickContactBadge;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-
-public class AttendeesAdapter extends BaseAdapter {
-    private static final String TAG = "AttendeesAdapter";
-    private static final boolean DEBUG = false;
-
-    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;
-
-    private static final String[] PRESENCE_PROJECTION = new String[] {
-        Email.CONTACT_ID,           // 0
-        Email.CONTACT_PRESENCE,     // 1
-        Email.DATA,                 // 2
-        Email.PHOTO_ID,             // 3
-    };
-
-    private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI;
-    private static final String CONTACT_DATA_SELECTION = Email.DATA + " IN (?)";
-
-    private Context mContext;
-    private Rfc822Validator mValidator;
-    private LayoutInflater mInflater;
-
-    private int mYes = 0;
-    private int mNo = 0;
-    private int mMaybe = 0;
-    private int mNoResponse = 0;
-
-    private Drawable mDefaultBadge;
-
-    private ArrayList<AttendeeItem> mAttendees;
-    private AttendeeItem[] mDividers = new AttendeeItem[4];
-    private RemoveAttendeeClickListener mRemoveListener = new RemoveAttendeeClickListener();
-    private PresenceQueryHandler mPresenceQueryHandler;
-
-    private class RemoveAttendeeClickListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            AttendeeItem item = (AttendeeItem) v.getTag();
-            item.mRemoved = !item.mRemoved;
-            notifyDataSetChanged();
-        }
-
-    }
-
-    public AttendeesAdapter(Context context, Rfc822Validator validator) {
-        Resources res = context.getResources();
-        mContext = context;
-        mValidator = validator;
-        mAttendees = new ArrayList<AttendeeItem>();
-        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mDefaultBadge = res.getDrawable(R.drawable.ic_contact_picture);
-
-        // Add the labels we need to our adapter
-        CharSequence[] entries;
-        entries = res.getTextArray(R.array.response_labels1);
-        // Yes
-        mDividers[0] = addDivider(context, entries[1]);
-        // No
-        mDividers[1] = addDivider(context, entries[3]);
-        // Maybe
-        mDividers[2] = addDivider(context, entries[2]);
-        // No Response
-        mDividers[3] = addDivider(context, entries[0]);
-
-        mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
-    }
-
-    private AttendeeItem addDivider(Context context, CharSequence label) {
-        AttendeeItem labelItem = new AttendeeItem();
-        labelItem.mRemoved = true;
-        labelItem.mDivider = true;
-        labelItem.mDividerLabel = label.toString();
-        mAttendees.add(labelItem);
-        return labelItem;
-    }
-
-    public boolean contains(Attendee attendee) {
-        for (AttendeeItem item : mAttendees) {
-            if (item.mAttendee == null) {
-                continue;
-            }
-            if(TextUtils.equals(attendee.mEmail, item.mAttendee.mEmail)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void addAttendee(Attendee attendee) {
-        int i = 0;
-        if (contains(attendee)) {
-            return;
-        }
-        int status = attendee.mStatus;
-        AttendeeItem startItem;
-        if (status == Attendees.ATTENDEE_STATUS_ACCEPTED) {
-            startItem = mDividers[0];
-            mYes++;
-        } else if (status == Attendees.ATTENDEE_STATUS_DECLINED) {
-            startItem = mDividers[1];
-            mNo++;
-        } else if (status == Attendees.ATTENDEE_STATUS_TENTATIVE){
-            startItem = mDividers[2];
-            mMaybe++;
-        } else {
-            startItem = mDividers[3];
-            mNoResponse++;
-        }
-        // Advance to the start of the section this name should go in
-        while (mAttendees.get(i++) != startItem);
-        int size = mAttendees.size();
-        String name = attendee.mName;
-        if (name == null) {
-            name = "";
-        }
-        while (true) {
-            if (i >= size) {
-                break;
-            }
-            AttendeeItem currItem = mAttendees.get(i);
-            if (currItem.mDivider) {
-                break;
-            }
-            if (name.compareToIgnoreCase(currItem.mAttendee.mName) < 0) {
-                break;
-            }
-            i++;
-        }
-        AttendeeItem item = new AttendeeItem();
-        item.mAttendee = attendee;
-        item.mPresence = -1;
-        item.mBadge = mDefaultBadge;
-        mAttendees.add(i, item);
-        // If we have any yes or no responses turn labels on where necessary
-        if (mYes > 0 || mNo > 0 || mMaybe > 0) {
-            startItem.mRemoved = false; // mView.setVisibility(View.VISIBLE);
-            if (DEBUG) {
-                Log.d(TAG, "Set " + startItem.mDividerLabel + " to visible");
-            }
-            if (mNoResponse > 0) {
-                mDividers[3].mRemoved = false; // mView.setVisibility(View.VISIBLE);
-                if (DEBUG) {
-                    Log.d(TAG, "Set " + mDividers[2].mDividerLabel + " to visible");
-                }
-            }
-        }
-        mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item,
-                CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION,
-                new String[] { attendee.mEmail }, null);
-        notifyDataSetChanged();
-    }
-
-    public void addAttendees(ArrayList<Attendee> attendees) {
-        synchronized (mAttendees) {
-            for (Attendee attendee : attendees) {
-                addAttendee(attendee);
-            }
-        }
-    }
-
-    public void addAttendees(HashMap<String, Attendee> attendees) {
-        synchronized (mAttendees) {
-            for (Attendee attendee : attendees.values()) {
-                addAttendee(attendee);
-            }
-        }
-    }
-
-    public void addAttendees(String attendees) {
-        LinkedHashSet<Rfc822Token> addresses =
-            EditEventHelper.getAddressesFromList(attendees, mValidator);
-        synchronized (mAttendees) {
-            for (Rfc822Token address : addresses) {
-                Attendee attendee = new Attendee(address.getName(), address.getAddress());
-                if (TextUtils.isEmpty(attendee.mName)) {
-                    attendee.mName = attendee.mEmail;
-                }
-                addAttendee(attendee);
-            }
-        }
-    }
-
-    public void removeAttendee(int position) {
-        if (position < 0) {
-            return;
-        }
-        AttendeeItem item = mAttendees.get(position);
-        if (item.mDivider) {
-            return;
-        }
-        int status = item.mAttendee.mStatus;
-        if (status == Attendees.ATTENDEE_STATUS_ACCEPTED) {
-            mYes--;
-            if (mYes == 0) {
-                mDividers[0].mRemoved = true;
-            }
-        } else if (status == Attendees.ATTENDEE_STATUS_DECLINED) {
-            mNo--;
-            if (mNo == 0) {
-                mDividers[1].mRemoved = true;
-            }
-        } else if (status == Attendees.ATTENDEE_STATUS_TENTATIVE) {
-            mMaybe--;
-        } else {
-            mNoResponse--;
-        }
-        if ((mYes == 0 && mNo == 0 && mMaybe == 0) || mNoResponse == 0) {
-            mDividers[3].mRemoved = true;
-        }
-        mAttendees.remove(position);
-    }
-
-    public int findAttendeeByEmail(String email) {
-        int size = mAttendees.size();
-        for (int i = 0; i < size; i++) {
-            AttendeeItem item = mAttendees.get(i);
-            if (item.mDivider) {
-                continue;
-            }
-            if (TextUtils.equals(email, item.mAttendee.mEmail)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    @Override
-    public int getCount() {
-        return mAttendees.size();
-    }
-
-    @Override
-    public boolean areAllItemsEnabled() {
-        return false;
-    }
-
-    @Override
-    public Attendee getItem(int position) {
-        return mAttendees.get(position).mAttendee;
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return position;
-    }
-
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        AttendeeItem item = mAttendees.get(position);
-
-        if (item.mDivider) {
-            TextView tv =  new TextView(mContext);
-            tv.setText(item.mDividerLabel);
-            tv.setTextAppearance(mContext, R.style.TextAppearance_EventInfo_Label);
-            tv.setVisibility(item.mRemoved ? View.GONE : View.VISIBLE);
-            return tv;
-        }
-
-        View v = mInflater.inflate(R.layout.contact_item, null);
-        Attendee attendee = item.mAttendee;
-
-        TextView nameView = (TextView)v.findViewById(R.id.name);
-        String name = attendee.mName;
-        if (name == null || name.length() == 0) {
-            name = attendee.mEmail;
-        }
-        nameView.setText(name);
-        if (item.mRemoved) {
-            nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
-        }
-
-        ImageButton button = (ImageButton) v.findViewById(R.id.contact_remove);
-        button.setVisibility(View.VISIBLE);
-        button.setTag(item);
-        if (item.mRemoved) {
-            button.setImageResource(R.drawable.ic_btn_round_plus);
-        }
-        button.setOnClickListener(mRemoveListener);
-
-        QuickContactBadge badge = (QuickContactBadge)v.findViewById(R.id.badge);
-        badge.setImageDrawable(item.mBadge);
-        badge.assignContactFromEmail(item.mAttendee.mEmail, true);
-
-        if (item.mPresence != -1) {
-            ImageView presence = (ImageView) v.findViewById(R.id.presence);
-            presence.setImageResource(Presence.getPresenceIconResourceId(item.mPresence));
-            presence.setVisibility(View.VISIBLE);
-
-        }
-        return v;
-    }
-
-    @Override
-    public boolean isEnabled(int position) {
-        return !mAttendees.get(position).mDivider;
-    }
-
-    /**
-     * 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) {
-        // First perform lookup query to find existing contact
-        final ContentResolver resolver = mContext.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(mContext, 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);
-            }
-
-            mContext.startActivity(intent);
-        }
-    }
-
-    public boolean isRemoved(int position) {
-        return mAttendees.get(position).mRemoved;
-    }
-
-    // TODO put this into a Loader for auto-requeries
-    private class PresenceQueryHandler extends AsyncQueryHandler {
-
-        public PresenceQueryHandler(ContentResolver cr) {
-            super(cr);
-        }
-
-        @Override
-        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
-            if (cursor == null || cookie == null) {
-                if (DEBUG) {
-                    Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
-                }
-                return;
-            }
-            AttendeeItem item = (AttendeeItem)cookie;
-
-            try {
-                cursor.moveToPosition(-1);
-                boolean found = false;
-                int contactId = 0;
-                int photoId = 0;
-                int presence = 0;
-                while (cursor.moveToNext()) {
-                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
-                    int temp = 0;
-                    temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
-                    // A photo id must be > 0 and we only care about the contact
-                    // ID if there's a photo
-                    if (temp > 0) {
-                        photoId = temp;
-                        contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
-                    }
-                    // Take the most available status we can find.
-                    presence = Math.max(
-                            cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence);
-
-                    found = true;
-                    if (DEBUG) {
-                        Log.d(TAG,
-                                "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
-                                        + " Email: " + email + " updateCount:" + item.mUpdateCounts
-                                        + " Presence:" + item.mPresence);
-                    }
-                }
-                if (found) {
-                    item.mPresence = presence;
-                    notifyDataSetChanged();
-
-                    if (photoId > 0 && item.mUpdateCounts < queryIndex) {
-                        item.mUpdateCounts = queryIndex;
-                        Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
-                                contactId);
-                        // Query for this contacts picture
-                        ContactsAsyncHelper.retrieveContactPhotoAsync(
-                                mContext, item, new Runnable() {
-                                    public void run() {
-                                        notifyDataSetChanged();
-                                    }
-                                }, personUri);
-                    }
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-    }
-}
diff --git a/src/com/android/calendar/event/AttendeesView.java b/src/com/android/calendar/event/AttendeesView.java
new file mode 100644
index 0000000..2d9917f
--- /dev/null
+++ b/src/com/android/calendar/event/AttendeesView.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calendar.event;
+
+import com.android.calendar.CalendarEventModel.Attendee;
+import com.android.calendar.ContactsAsyncHelper;
+import com.android.calendar.R;
+import com.android.calendar.event.EditEventHelper.AttendeeItem;
+import com.android.common.Rfc822Validator;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.Calendar.Attendees;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.StatusUpdates;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+
+public class AttendeesView extends LinearLayout implements View.OnClickListener {
+    private static final String TAG = "AttendeesView";
+    private static final boolean DEBUG = false;
+
+    private static final int PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0;
+    private static final int PRESENCE_PROJECTION_PRESENCE_INDEX = 1;
+    private static final int PRESENCE_PROJECTION_EMAIL_INDEX = 2;
+    private static final int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3;
+
+    private static final String[] PRESENCE_PROJECTION = new String[] {
+        Email.CONTACT_ID,           // 0
+        Email.CONTACT_PRESENCE,     // 1
+        Email.DATA,                 // 2
+        Email.PHOTO_ID,             // 3
+    };
+
+    private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI;
+    private static final String CONTACT_DATA_SELECTION = Email.DATA + " IN (?)";
+
+    private final Context mContext;
+    private final LayoutInflater mInflater;
+    private final PresenceQueryHandler mPresenceQueryHandler;
+    private final Drawable mDefaultBadge;
+
+    // TextView shown at the top of each type of attendees
+    // e.g.
+    // Yes  <-- divider
+    // example_for_yes <exampleyes@example.com>
+    // No <-- divider
+    // example_for_no <exampleno@example.com>
+    private final View mDividerForYes;
+    private final View mDividerForNo;
+    private final View mDividerForMaybe;
+    private final View mDividerForNoResponse;
+
+    private Rfc822Validator mValidator;
+
+    // Number of attendees responding or not responding.
+    private int mYes;
+    private int mNo;
+    private int mMaybe;
+    private int mNoResponse;
+
+    public AttendeesView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
+
+        final Resources resources = context.getResources();
+        mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture);
+
+        final CharSequence[] entries = resources.getTextArray(R.array.response_labels1);
+        mDividerForYes = constructDividerView(entries[1]);
+        mDividerForNo = constructDividerView(entries[3]);
+        mDividerForMaybe = constructDividerView(entries[2]);
+        mDividerForNoResponse = constructDividerView(entries[0]);
+    }
+
+    public void setRfc822Validator(Rfc822Validator validator) {
+        mValidator = validator;
+    }
+
+    private View constructDividerView(CharSequence label) {
+        final TextView textView = new TextView(mContext);
+        textView.setText(label);
+        textView.setTextAppearance(mContext, R.style.TextAppearance_EventInfo_Label);
+        textView.setClickable(false);
+        return textView;
+    }
+
+    /**
+     * Inflates a layout for a given attendee view and set up each element in it, and returns
+     * the constructed View object. The object is also stored in {@link AttendeeItem#mView}.
+     */
+    private View constructAttendeeView(AttendeeItem item) {
+        final Attendee attendee = item.mAttendee;
+        item.mView = mInflater.inflate(R.layout.contact_item, null);
+        return updateAttendeeView(item);
+    }
+
+    /**
+     * Set up each element in {@link AttendeeItem#mView} using the latest information. View
+     * object is reused.
+     */
+    private View updateAttendeeView(AttendeeItem item) {
+        final Attendee attendee = item.mAttendee;
+        final View view = item.mView;
+        final TextView nameView = (TextView)view.findViewById(R.id.name);
+        nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName);
+        if (item.mRemoved) {
+            nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
+        } else {
+            nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags());
+        }
+
+        final ImageButton button = (ImageButton)view.findViewById(R.id.contact_remove);
+        button.setVisibility(View.VISIBLE);
+        button.setTag(item);
+        if (item.mRemoved) {
+            button.setImageResource(R.drawable.ic_btn_round_plus);
+        } else {
+            button.setImageResource(R.drawable.ic_btn_round_minus);
+        }
+        button.setOnClickListener(this);
+
+        final QuickContactBadge badge = (QuickContactBadge)view.findViewById(R.id.badge);
+        badge.setImageDrawable(item.mBadge);
+        badge.assignContactFromEmail(item.mAttendee.mEmail, true);
+        badge.setMaxHeight(60);
+        if (item.mPresence != -1) {
+            final ImageView presence = (ImageView) view.findViewById(R.id.presence);
+            presence.setImageResource(StatusUpdates.getPresenceIconResourceId(item.mPresence));
+            presence.setVisibility(View.VISIBLE);
+
+        }
+
+        return view;
+    }
+
+    public boolean contains(Attendee attendee) {
+        final int size = getChildCount();
+        for (int i = 0; i < size; i++) {
+            final View view = getChildAt(i);
+            if (view instanceof TextView) {  // divider
+                continue;
+            }
+            AttendeeItem attendeeItem = (AttendeeItem)view.getTag();
+            if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void addOneAttendee(Attendee attendee) {
+        if (contains(attendee)) {
+            return;
+        }
+        final AttendeeItem item = new AttendeeItem(attendee, -1 /* presence */, mDefaultBadge);
+        final int status = attendee.mStatus;
+        final String name = attendee.mName == null ? "" : attendee.mName;
+        final int index;
+        switch (status) {
+        case Attendees.ATTENDEE_STATUS_ACCEPTED: {
+            final int startIndex = 0;
+            if (mYes == 0) {
+                addView(mDividerForYes, startIndex);
+            }
+            mYes++;
+            index = startIndex + mYes;
+            break;
+        }
+        case Attendees.ATTENDEE_STATUS_DECLINED: {
+            final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
+            if (mNo == 0) {
+                addView(mDividerForNo, startIndex);
+            }
+            mNo++;
+            index = startIndex + mNo;
+            break;
+        }
+        case Attendees.ATTENDEE_STATUS_TENTATIVE: {
+            final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
+            if (mMaybe == 0) {
+                addView(mDividerForMaybe, startIndex);
+            }
+            mMaybe++;
+            index = startIndex + mMaybe;
+            break;
+        }
+        default: {
+            final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) +
+                    (mMaybe == 0 ? 0 : 1 + mMaybe);
+            // We delay adding the divider for "No response".
+            index = startIndex + mNoResponse;
+            mNoResponse++;
+            break;
+        }
+        }
+
+        final View view = constructAttendeeView(item);
+        view.setTag(item);
+        addView(view, index);
+
+        // We want "No Response" divider only when
+        // - someone already answered in some way,
+        // - there is attendees not responding yet, and
+        // - divider isn't in the list yet
+        if (mYes + mNo + mMaybe > 0 && mNoResponse > 0 &&
+                mDividerForNoResponse.getParent() == null) {
+            final int dividerIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) +
+                    (mMaybe == 0 ? 0 : 1 + mMaybe);
+            addView(mDividerForNoResponse, dividerIndex);
+        }
+
+        mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item,
+                CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION,
+                new String[] { attendee.mEmail }, null);
+    }
+
+    public void addAttendees(ArrayList<Attendee> attendees) {
+        synchronized (this) {
+            for (final Attendee attendee : attendees) {
+                addOneAttendee(attendee);
+            }
+        }
+    }
+
+    public void addAttendees(HashMap<String, Attendee> attendees) {
+        synchronized (this) {
+            for (final Attendee attendee : attendees.values()) {
+                addOneAttendee(attendee);
+            }
+        }
+    }
+
+    public void addAttendees(String attendees) {
+        final LinkedHashSet<Rfc822Token> addresses =
+                EditEventHelper.getAddressesFromList(attendees, mValidator);
+        synchronized (this) {
+            for (final Rfc822Token address : addresses) {
+                final Attendee attendee = new Attendee(address.getName(), address.getAddress());
+                if (TextUtils.isEmpty(attendee.mName)) {
+                    attendee.mName = attendee.mEmail;
+                }
+                addOneAttendee(attendee);
+            }
+        }
+    }
+
+    /**
+     * Returns true when the attendee at that index is marked as "removed" (the name of
+     * the attendee is shown with a strike through line).
+     */
+    public boolean isMarkAsRemoved(int index) {
+        final View view = getChildAt(index);
+        if (view instanceof TextView) {  // divider
+            return false;
+        }
+        return ((AttendeeItem)view.getTag()).mRemoved;
+    }
+
+    // TODO put this into a Loader for auto-requeries
+    private class PresenceQueryHandler extends AsyncQueryHandler {
+        public PresenceQueryHandler(ContentResolver cr) {
+            super(cr);
+        }
+
+        @Override
+        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
+            if (cursor == null || cookie == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
+                }
+                return;
+            }
+
+            final AttendeeItem item = (AttendeeItem)cookie;
+            try {
+                cursor.moveToPosition(-1);
+                boolean found = false;
+                int contactId = 0;
+                int photoId = 0;
+                int presence = 0;
+                while (cursor.moveToNext()) {
+                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
+                    int temp = 0;
+                    temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
+                    // A photo id must be > 0 and we only care about the contact
+                    // ID if there's a photo
+                    if (temp > 0) {
+                        photoId = temp;
+                        contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
+                    }
+                    // Take the most available status we can find.
+                    presence = Math.max(
+                            cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence);
+
+                    found = true;
+                    if (DEBUG) {
+                        Log.d(TAG,
+                                "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
+                                        + " Email: " + email + " updateCount:" + item.mUpdateCounts
+                                        + " Presence:" + item.mPresence);
+                    }
+                }
+                if (found) {
+                    item.mPresence = presence;
+
+                    if (photoId > 0 && item.mUpdateCounts < queryIndex) {
+                        item.mUpdateCounts = queryIndex;
+                        final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+                                contactId);
+                        // Query for this contacts picture
+                        ContactsAsyncHelper.retrieveContactPhotoAsync(
+                                mContext, item, new Runnable() {
+                                    public void run() {
+                                        updateAttendeeView(item);
+                                    }
+                                }, personUri);
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    public Attendee getItem(int index) {
+        final View view = getChildAt(index);
+        if (view instanceof TextView) {  // divider
+            return null;
+        }
+        return ((AttendeeItem)view.getTag()).mAttendee;
+    }
+
+    @Override
+    public void onClick(View view) {
+        // Button corresponding to R.id.contact_remove.
+        final AttendeeItem item = (AttendeeItem) view.getTag();
+        item.mRemoved = !item.mRemoved;
+        updateAttendeeView(item);
+    }
+}
diff --git a/src/com/android/calendar/event/EditEventFragment.java b/src/com/android/calendar/event/EditEventFragment.java
index b7026b9..c6fce2a 100644
--- a/src/com/android/calendar/event/EditEventFragment.java
+++ b/src/com/android/calendar/event/EditEventFragment.java
@@ -265,7 +265,7 @@
         }
     }
 
-    void setModelIfDone(int queryType) {
+    private void setModelIfDone(int queryType) {
         synchronized (this) {
             mOutstandingQueries &= ~queryType;
             if (mOutstandingQueries == 0) {
diff --git a/src/com/android/calendar/event/EditEventHelper.java b/src/com/android/calendar/event/EditEventHelper.java
index 33e5cd1..bf44c45 100644
--- a/src/com/android/calendar/event/EditEventHelper.java
+++ b/src/com/android/calendar/event/EditEventHelper.java
@@ -206,12 +206,17 @@
 
     public static class AttendeeItem {
         public boolean mRemoved;
-        public boolean mDivider;
-        public String mDividerLabel;
         public Attendee mAttendee;
         public Drawable mBadge;
         public int mPresence;
         public int mUpdateCounts;
+        public View mView;
+
+        public AttendeeItem(Attendee attendee, int presence, Drawable badge) {
+            mAttendee = attendee;
+            mPresence = presence;
+            mBadge = badge;
+        }
     }
 
     public EditEventHelper(Context context, CalendarEventModel model) {
diff --git a/src/com/android/calendar/event/EditEventView.java b/src/com/android/calendar/event/EditEventView.java
index 4b108e0..53f5207 100644
--- a/src/com/android/calendar/event/EditEventView.java
+++ b/src/com/android/calendar/event/EditEventView.java
@@ -78,13 +78,9 @@
 
 public class EditEventView implements View.OnClickListener, DialogInterface.OnCancelListener,
         DialogInterface.OnClickListener {
-
     private static final String TAG = "EditEvent";
-
     private static final String GOOGLE_SECONDARY_CALENDAR = "calendar.google.com";
-
     private static final int REMINDER_FLING_VELOCITY = 2000;
-
     private LayoutInflater mLayoutInflater;
 
     ArrayList<View> mViewList = new ArrayList<View>();
@@ -112,8 +108,7 @@
     LinearLayout mRemindersContainer;
     MultiAutoCompleteTextView mAttendeesList;
     ImageButton mAddAttendeesButton;
-    ListView mGuestList;
-    AttendeesAdapter mAttendeesAdapter;
+    AttendeesView mAttendeesView;
     AddAttendeeClickListener mAddAttendeesListener;
 
     private ProgressDialog mLoadingCalendarsDialog;
@@ -202,9 +197,9 @@
             // add button wasn't clicked e.g. when the Save button is clicked.
             // The mAttendeesList may not be setup since the user doesn't have
             // permission to add attendees.
-            if (mAttendeesList != null && mAttendeesAdapter != null) {
+            if (mAttendeesList != null) {
                 mAttendeesList.performValidation();
-                mAttendeesAdapter.addAttendees(mAttendeesList.getText().toString());
+                mAttendeesView.addAttendees(mAttendeesList.getText().toString());
                 mAttendeesList.setText("");
             }
         }
@@ -557,13 +552,13 @@
     // This is called if the user clicks on one of the buttons: "Save",
     // "Discard", or "Delete". This is also called if the user clicks
     // on the "remove reminder" button.
-    public void onClick(View v) {
-        if (v == mSaveButton) {
+    public void onClick(View view) {
+        if (view == mSaveButton) {
             // If we're creating a new event but haven't gotten any calendars
             // yet let the user know we're waiting for calendars to finish
             // loading. The save button isn't enabled until we have a non-null
             // mModel.
-            mAddAttendeesListener.onClick(v);
+            mAddAttendeesListener.onClick(view);
             if (mCalendarsCursor == null && mModel.mUri == null) {
                 if (mLoadingCalendarsDialog == null) {
                     // Create the progress dialog
@@ -581,22 +576,18 @@
                 mDone.run();
             }
             return;
-        }
-
-        if (v == mDeleteButton) {
+        } else if (view == mDeleteButton) {
             mDone.setDoneCode(Utils.DONE_DELETE | Utils.DONE_EXIT);
             mDone.run();
             return;
-        }
-
-        if (v == mDiscardButton) {
+        } else if (view == mDiscardButton) {
             mDone.setDoneCode(Utils.DONE_REVERT);
             mDone.run();
             return;
         }
 
         // This must be a click on one of the "remove reminder" buttons
-        LinearLayout reminderItem = (LinearLayout) v.getParent();
+        LinearLayout reminderItem = (LinearLayout) view.getParent();
         LinearLayout parent = (LinearLayout) reminderItem.getParent();
         parent.removeView(reminderItem);
         mReminderItems.remove(reminderItem);
@@ -617,6 +608,7 @@
     }
 
     // This is called if the user clicks on a dialog button.
+    @Override
     public void onClick(DialogInterface dialog, int which) {
         if (dialog == mNoCalendarsDialog) {
             mDone.setDoneCode(Utils.DONE_REVERT);
@@ -652,18 +644,15 @@
             mModel.mSelfAttendeeStatus = EditEventHelper.ATTENDEE_VALUES[position];
         }
 
-        if (mGuestList != null) {
-            AttendeesAdapter adapter = mAttendeesAdapter;
-            if (adapter != null && !adapter.isEmpty()) {
-                int size = adapter.getCount();
-                mModel.mAttendeesList.clear();
-                for (int i = 0; i < size; i++) {
-                    Attendee attendee = adapter.getItem(i);
-                    if (attendee == null || adapter.isRemoved(i)) {
-                        continue;
-                    }
-                    mModel.addAttendee(attendee);
+        if (mAttendeesView != null && mAttendeesView.getChildCount() > 0) {
+            final int size = mAttendeesView.getChildCount();
+            mModel.mAttendeesList.clear();
+            for (int i = 0; i < size; i++) {
+                final Attendee attendee = mAttendeesView.getItem(i);
+                if (attendee == null || mAttendeesView.isMarkAsRemoved(i)) {
+                    continue;
                 }
+                mModel.addAttendee(attendee);
             }
         }
 
@@ -791,7 +780,7 @@
         mTimezone = TimeZone.getDefault().getID();
         mTimezoneAdapter = new TimezoneAdapter(mActivity, mTimezone);
 
-        mGuestList = (ListView) view.findViewById(R.id.attendee_list);
+        mAttendeesView = (AttendeesView)view.findViewById(R.id.attendee_list);
 
         // Display loading screen
         setModel(null);
@@ -1137,15 +1126,9 @@
         return 0;
     }
 
-    // FRAG_TODO convert this from listview to linearlayout
     public void updateAttendees(HashMap<String, Attendee> attendeesList) {
-        if (mAttendeesAdapter == null) {
-            mAttendeesAdapter = new AttendeesAdapter(mActivity, mEmailValidator);
-        }
-        if (attendeesList.size() > 0) {
-            mAttendeesAdapter.addAttendees(attendeesList);
-        }
-        mGuestList.setAdapter(mAttendeesAdapter);
+        mAttendeesView.setRfc822Validator(mEmailValidator);
+        mAttendeesView.addAttendees(attendeesList);
     }
 
     private void updateRemindersVisibility(int numReminders) {