Group editor on tablet

- Allow autocomplete to add new members which presents
the user with suggestions from the raw_contacts table
with the same account name and type as the group
- Hook up the "new" and "edit" group buttons on the tablet
- Once the user exits the editor, update the group list
and scroll the group list to the group that was just edited
- Allow rename of groups (make the names of read-only groups
not editable)
- Allow removal of members
- Hook up the done / cancel / up / back buttons

- TODO: Be able to create a new group + add new members
in the same transaction. Then the new group editor
will allow adding members at the same time. Currently
you can only add a name to a new group. Once it's created,
then you can go and edit the membership.

- TODO: Bulk add/remove members in one transaction when the
user exits the editor. Currently it's saving the change
after you modify the membership list (even before you hit
the "Done" button in the editor).

- TODO: Add member status message and chat presence

- TODO: Add UI for non-editable groups

Change-Id: I1f32a28862c358b8bd1469666743cd240d28f80b
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 9b56f5b..d8dbdbc 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -396,6 +396,7 @@
         values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(groupUri));
 
         Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+        callbackIntent.setData(groupUri);
         callbackIntent.putExtra(ContactsContract.Intents.Insert.DATA, Lists.newArrayList(values));
 
         deliverCallback(callbackIntent);
@@ -404,11 +405,18 @@
     /**
      * Creates an intent that can be sent to this service to rename a group.
      */
-    public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel) {
+    public static Intent createGroupRenameIntent(Context context, long groupId, String newLabel,
+            Class<?> callbackActivity, String callbackAction) {
         Intent serviceIntent = new Intent(context, ContactSaveService.class);
         serviceIntent.setAction(ContactSaveService.ACTION_RENAME_GROUP);
         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);
         serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_LABEL, newLabel);
+
+        // Callback intent will be invoked by the service once the group is renamed.
+        Intent callbackIntent = new Intent(context, callbackActivity);
+        callbackIntent.setAction(callbackAction);
+        serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
+
         return serviceIntent;
     }
 
@@ -423,8 +431,12 @@
 
         ContentValues values = new ContentValues();
         values.put(Groups.TITLE, label);
-        getContentResolver().update(
-                ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), values, null, null);
+        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
+        getContentResolver().update(groupUri, values, null, null);
+
+        Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
+        callbackIntent.setData(groupUri);
+        deliverCallback(callbackIntent);
     }
 
     /**
diff --git a/src/com/android/contacts/GroupMetaDataLoader.java b/src/com/android/contacts/GroupMetaDataLoader.java
index 927697a..d900825 100644
--- a/src/com/android/contacts/GroupMetaDataLoader.java
+++ b/src/com/android/contacts/GroupMetaDataLoader.java
@@ -33,6 +33,7 @@
         Groups.TITLE,
         Groups.AUTO_ADD,
         Groups.FAVORITES,
+        Groups.GROUP_IS_READ_ONLY,
     };
 
     public final static int ACCOUNT_NAME = 0;
@@ -41,6 +42,7 @@
     public final static int TITLE = 3;
     public final static int AUTO_ADD = 4;
     public final static int FAVORITES = 5;
+    public final static int IS_READ_ONLY = 6;
 
     public GroupMetaDataLoader(Context context, Uri groupUri) {
         super(context, ensureIsGroupUri(groupUri), COLUMNS, Groups.ACCOUNT_TYPE + " NOT NULL AND "
diff --git a/src/com/android/contacts/activities/GroupDetailActivity.java b/src/com/android/contacts/activities/GroupDetailActivity.java
index 95f804c..ee1af57 100644
--- a/src/com/android/contacts/activities/GroupDetailActivity.java
+++ b/src/com/android/contacts/activities/GroupDetailActivity.java
@@ -20,6 +20,7 @@
 import com.android.contacts.R;
 import com.android.contacts.group.GroupDetailFragment;
 
+import android.net.Uri;
 import android.os.Bundle;
 
 public class GroupDetailActivity extends ContactsActivity {
@@ -55,5 +56,10 @@
         public void onGroupTitleUpdated(String title) {
             getActionBar().setTitle(title);
         }
+
+        @Override
+        public void onEditRequested(Uri groupUri) {
+            // TODO: Disabling editor activity for phone right now because it's not ready
+        }
     };
 }
diff --git a/src/com/android/contacts/activities/GroupEditorActivity.java b/src/com/android/contacts/activities/GroupEditorActivity.java
new file mode 100644
index 0000000..2a79852
--- /dev/null
+++ b/src/com/android/contacts/activities/GroupEditorActivity.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2011 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.contacts.activities;
+
+import com.android.contacts.ContactsActivity;
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.editor.ContactEditorFragment.SaveMode;
+import com.android.contacts.group.GroupEditorFragment;
+import com.android.contacts.util.DialogManager;
+
+import android.app.ActionBar;
+import android.app.Dialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuItem;
+
+public class GroupEditorActivity extends ContactsActivity
+        implements DialogManager.DialogShowingViewActivity {
+
+    private static final String TAG = "GroupEditorActivity";
+
+    public static final String ACTION_SAVE_COMPLETED = "saveCompleted";
+    public static final String ACTION_ADD_MEMBER_COMPLETED = "addMemberCompleted";
+    public static final String ACTION_REMOVE_MEMBER_COMPLETED = "removeMemberCompleted";
+
+    private GroupEditorFragment mFragment;
+
+    private DialogManager mDialogManager = new DialogManager(this);
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        String action = getIntent().getAction();
+
+        if (ACTION_SAVE_COMPLETED.equals(action)) {
+            finish();
+            return;
+        }
+
+        setContentView(R.layout.group_editor_activity);
+
+        // This Activity will always fall back to the "top" Contacts screen when touched on the
+        // app up icon, regardless of launch context.
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
+
+        mFragment = (GroupEditorFragment) getFragmentManager().findFragmentById(
+                R.id.group_editor_fragment);
+        mFragment.setListener(mFragmentListener);
+        mFragment.setContentResolver(getContentResolver());
+        Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null;
+        mFragment.load(action, uri);
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        if (DialogManager.isManagedId(id)) {
+            return mDialogManager.onCreateDialog(id, args);
+        } else {
+            // Nobody knows about the Dialog
+            Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
+            return null;
+        }
+    }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        if (globalSearch) {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        } else {
+            ContactsSearchManager.startSearch(this, initialQuery);
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        mFragment.save(SaveMode.CLOSE);
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+
+        if (mFragment == null) {
+            return;
+        }
+
+        String action = intent.getAction();
+        if (ACTION_SAVE_COMPLETED.equals(action)) {
+            mFragment.onSaveCompleted(true,
+                    intent.getIntExtra(GroupEditorFragment.SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE),
+                    intent.getData());
+        } else if (ACTION_ADD_MEMBER_COMPLETED.equals(action)) {
+            mFragment.finishAddMember(intent.getData());
+        } else if (ACTION_REMOVE_MEMBER_COMPLETED.equals(action)) {
+            mFragment.finishRemoveMember(intent.getData());
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home: {
+                mFragment.save(SaveMode.HOME);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private final GroupEditorFragment.Listener mFragmentListener =
+            new GroupEditorFragment.Listener() {
+        @Override
+        public void onGroupNotFound() {
+            finish();
+        }
+
+        @Override
+        public void onReverted() {
+            finish();
+        }
+
+        @Override
+        public void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome) {
+            setResult(resultCode, resultIntent);
+            if (navigateHome) {
+                Intent intent = new Intent(GroupEditorActivity.this, PeopleActivity.class);
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                startActivity(intent);
+            }
+            finish();
+        }
+
+        @Override
+        public void onTitleLoaded(int resourceId) {
+            setTitle(resourceId);
+        }
+    };
+
+    @Override
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
+}
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 0bec6cc..07c9a48 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -92,7 +92,10 @@
 
     private static final int SUBACTIVITY_NEW_CONTACT = 2;
     private static final int SUBACTIVITY_EDIT_CONTACT = 3;
-    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 4;
+    private static final int SUBACTIVITY_NEW_GROUP = 4;
+    private static final int SUBACTIVITY_EDIT_GROUP = 5;
+    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 6;
+
     private static final int FAVORITES_COLUMN_COUNT = 4;
 
     private static final String KEY_SEARCH_MODE = "searchMode";
@@ -115,8 +118,10 @@
     private boolean mContentPaneDisplayed;
 
     private ContactDetailFragment mContactDetailFragment;
-    private ContactDetailFragmentListener mContactDetailFragmentListener =
+    private final ContactDetailFragmentListener mContactDetailFragmentListener =
             new ContactDetailFragmentListener();
+    private final GroupDetailFragmentListener mGroupDetailFragmentListener =
+            new GroupDetailFragmentListener();
 
     private GroupDetailFragment mGroupDetailFragment;
 
@@ -184,6 +189,7 @@
                     new ContactsUnavailableFragmentListener());
         } else if (fragment instanceof GroupDetailFragment) {
             mGroupDetailFragment = (GroupDetailFragment) fragment;
+            mGroupDetailFragment.setListener(mGroupDetailFragmentListener);
             mContentPaneDisplayed = true;
         } else if (fragment instanceof StrequentContactListFragment) {
             mFavoritesFragment = (StrequentContactListFragment) fragment;
@@ -811,6 +817,31 @@
         }
     }
 
+    private class GroupDetailFragmentListener implements GroupDetailFragment.Listener {
+        @Override
+        public void onGroupSizeUpdated(String size) {
+            // Nothing needs to be done here because the size will be displayed in the detail
+            // fragment
+        }
+
+        @Override
+        public void onGroupTitleUpdated(String title) {
+            // Nothing needs to be done here because the title will be displayed in the detail
+            // fragment
+        }
+
+        @Override
+        public void onEditRequested(Uri groupUri) {
+            // TODO: Send off an intent with the groups URI, so we don't need to specify
+            // the editor activity class. Then it would be declared as:
+            // new Intent(Intent.ACTION_EDIT, groupUri), SUBACTIVITY_EDIT_GROUP);
+            final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class);
+            intent.setData(groupUri);
+            intent.setAction(Intent.ACTION_EDIT);
+            startActivityForResult(intent, SUBACTIVITY_EDIT_GROUP);
+        }
+    }
+
     public void startActivityAndForwardResult(final Intent intent) {
         intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
 
@@ -957,8 +988,12 @@
                 return true;
             }
             case R.id.menu_add_group: {
-                // TODO: Hook up "new group" functionality
-                Toast.makeText(this, "NEW GROUP", Toast.LENGTH_SHORT).show();
+                // TODO: Send off an intent with the groups URI, so we don't need to specify
+                // the editor activity class. Then it would be declared as:
+                // new Intent(Intent.ACTION_INSERT, Groups.CONTENT_URI)
+                final Intent intent = new Intent(this, GroupEditorActivity.class);
+                intent.setAction(Intent.ACTION_INSERT);
+                startActivityForResult(intent, SUBACTIVITY_NEW_GROUP);
                 return true;
             }
             case R.id.menu_import_export: {
@@ -1007,6 +1042,15 @@
                 break;
             }
 
+            case SUBACTIVITY_NEW_GROUP:
+            case SUBACTIVITY_EDIT_GROUP: {
+                if (resultCode == RESULT_OK && mContentPaneDisplayed) {
+                    mRequest.setActionCode(ContactsRequest.ACTION_GROUP);
+                    mGroupsFragment.setSelectedUri(data.getData());
+                }
+                break;
+            }
+
             // TODO: Using the new startActivityWithResultFromFragment API this should not be needed
             // anymore
             case ContactEntryListFragment.ACTIVITY_REQUEST_CODE_PICKER:
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 91792df..2cd7b3c 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -125,6 +125,8 @@
     /**
      * Modes that specify what the AsyncTask has to perform after saving
      */
+    // TODO: Move this into a common utils class or the save service because the contact and
+    // group editors need to use this interface definition
     public interface SaveMode {
         /**
          * Close the editor after saving
@@ -697,7 +699,7 @@
 
     @Override
     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
-        inflater.inflate(R.menu.edit, menu);
+        inflater.inflate(R.menu.edit_contact, menu);
     }
 
     @Override
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index f530186..29aa737 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -71,6 +71,24 @@
         }
     }
 
+    public int getSelectedGroupPosition() {
+        if (mSelectedGroupUri == null) {
+            return -1;
+        }
+
+        int size = mGroupList.size();
+        for (int i = 0; i < size; i++) {
+            GroupListEntry group = mGroupList.get(i);
+            if (group.type == ViewType.ITEM) {
+                Uri uri = getGroupUriFromId(group.groupData.getGroupId());
+                if (mSelectedGroupUri.equals(uri)) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
     public void setSelectionVisible(boolean flag) {
         mSelectionVisible = flag;
     }
@@ -224,11 +242,15 @@
         public void loadFromGroup(GroupMetaData group) {
             mLabel.setText(group.getTitle());
             mAccount.setText(group.getAccountName());
-            mUri = ContentUris.withAppendedId(Groups.CONTENT_URI, group.getGroupId());
+            mUri = getGroupUriFromId(group.getGroupId());
         }
 
         public Uri getUri() {
             return mUri;
         }
     }
+
+    private static Uri getGroupUriFromId(long groupId) {
+        return ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/group/GroupBrowseListFragment.java b/src/com/android/contacts/group/GroupBrowseListFragment.java
index 958d9f8..afa13fa 100644
--- a/src/com/android/contacts/group/GroupBrowseListFragment.java
+++ b/src/com/android/contacts/group/GroupBrowseListFragment.java
@@ -20,6 +20,7 @@
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
 import com.android.contacts.group.GroupBrowseListAdapter.GroupListItem;
+import com.android.contacts.widget.AutoScrollListView;
 
 import android.app.Activity;
 import android.app.Fragment;
@@ -74,6 +75,8 @@
     private Context mContext;
     private Cursor mGroupListCursor;
 
+    private boolean mSelectionToScreenRequested;
+
     /**
      * Map of account name to a list of {@link GroupMetaData} objects
      * representing groups within that account.
@@ -83,11 +86,12 @@
     private Map<String, List<GroupMetaData>> mGroupMap = new HashMap<String, List<GroupMetaData>>();
 
     private View mRootView;
-    private ListView mListView;
+    private AutoScrollListView mListView;
     private View mEmptyView;
 
     private GroupBrowseListAdapter mAdapter;
     private boolean mSelectionVisible;
+    private Uri mSelectedGroupUri;
 
     private int mVerticalScrollbarPosition = View.SCROLLBAR_POSITION_RIGHT;
 
@@ -100,7 +104,7 @@
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         mRootView = inflater.inflate(R.layout.group_browse_list_fragment, null);
-        mListView = (ListView) mRootView.findViewById(R.id.list);
+        mListView = (AutoScrollListView) mRootView.findViewById(R.id.list);
         mListView.setOnFocusChangeListener(this);
         mListView.setOnTouchListener(this);
         mEmptyView = mRootView.findViewById(R.id.empty);
@@ -205,6 +209,7 @@
 
         mAdapter = new GroupBrowseListAdapter(mContext, mGroupMap);
         mAdapter.setSelectionVisible(mSelectionVisible);
+        mAdapter.setSelectedGroup(mSelectedGroupUri);
 
         mListView.setAdapter(mAdapter);
         mListView.setEmptyView(mEmptyView);
@@ -215,6 +220,10 @@
                 viewGroup(groupListItem.getUri());
             }
         });
+
+        if (mSelectionToScreenRequested) {
+            requestSelectionToScreen();
+        }
     }
 
     public void setListener(OnGroupBrowserActionListener listener) {
@@ -226,6 +235,7 @@
     }
 
     private void setSelectedGroup(Uri groupUri) {
+        mSelectedGroupUri = groupUri;
         mAdapter.setSelectedGroup(groupUri);
         mListView.invalidateViews();
     }
@@ -235,6 +245,18 @@
         if (mListener != null) mListener.onViewGroupAction(groupUri);
     }
 
+    public void setSelectedUri(Uri groupUri) {
+        viewGroup(groupUri);
+        mSelectionToScreenRequested = true;
+    }
+
+    protected void requestSelectionToScreen() {
+        int selectedPosition = mAdapter.getSelectedGroupPosition();
+        if (selectedPosition != -1) {
+            mListView.requestPositionToScreen(selectedPosition, true /* smooth scroll requested */);
+        }
+    }
+
     private void hideSoftKeyboard() {
         if (mContext == null) {
             return;
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index a0b7fb5..9784187 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -21,7 +21,6 @@
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
 import com.android.contacts.interactions.GroupDeletionDialogFragment;
-import com.android.contacts.interactions.GroupRenamingDialogFragment;
 import com.android.contacts.list.ContactTileAdapter;
 import com.android.contacts.list.ContactTileAdapter.DisplayType;
 
@@ -62,6 +61,11 @@
          * The number of group members has been determined
          */
         public void onGroupSizeUpdated(String size);
+
+        /**
+         * User decided to go to Edit-Mode
+         */
+        public void onEditRequested(Uri groupUri);
     }
 
     private static final String TAG = "GroupDetailFragment";
@@ -276,9 +280,6 @@
         final MenuItem editMenu = menu.findItem(R.id.menu_edit_group);
         editMenu.setVisible(mOptionsMenuEditable);
 
-        final MenuItem renameMenu = menu.findItem(R.id.menu_rename_group);
-        renameMenu.setVisible(mOptionsMenuEditable);
-
         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group);
         deleteMenu.setVisible(mOptionsMenuEditable);
     }
@@ -287,14 +288,9 @@
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.menu_edit_group: {
-                // TODO: Open group editor
-                Toast.makeText(mContext, "EDIT GROUP", Toast.LENGTH_SHORT).show();
+                if (mListener != null) mListener.onEditRequested(mGroupUri);
                 break;
             }
-            case R.id.menu_rename_group: {
-                GroupRenamingDialogFragment.show(getFragmentManager(), mGroupId, mGroupName);
-                return true;
-            }
             case R.id.menu_delete_group: {
                 GroupDeletionDialogFragment.show(getFragmentManager(), mGroupId, mGroupName);
                 return true;
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
new file mode 100644
index 0000000..0394d30
--- /dev/null
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (C) 2011 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.contacts.group;
+
+import com.android.contacts.ContactLoader;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.ContactSaveService;
+import com.android.contacts.GroupMemberLoader;
+import com.android.contacts.GroupMetaDataLoader;
+import com.android.contacts.R;
+import com.android.contacts.activities.GroupEditorActivity;
+import com.android.contacts.editor.ContactEditorFragment.SaveMode;
+import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.DataKind;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
+import com.android.internal.util.Objects;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.BaseAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO: Use savedInstanceState
+public class GroupEditorFragment extends Fragment {
+
+    private static final String TAG = "GroupEditorFragment";
+
+    private static final String LEGACY_CONTACTS_AUTHORITY = "contacts";
+
+    public static interface Listener {
+        /**
+         * Group metadata was not found, close the fragment now.
+         */
+        public void onGroupNotFound();
+
+        /**
+         * User has tapped Revert, close the fragment now.
+         */
+        void onReverted();
+
+        /**
+         * Title has been determined.
+         */
+        void onTitleLoaded(int resourceId);
+
+        /**
+         * Contact was saved and the Fragment can now be closed safely.
+         */
+        void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome);
+    }
+
+    private static final int LOADER_GROUP_METADATA = 1;
+    private static final int LOADER_EXISTING_MEMBERS = 2;
+    private static final int LOADER_NEW_GROUP_MEMBER = 3;
+    private static final int FULL_LOADER_NEW_GROUP_MEMBER = 4;
+
+    public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
+
+    private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri";
+    private static final String MEMBER_ACTION_KEY = "memberAction";
+
+    private static final int ADD_MEMBER = 0;
+    private static final int REMOVE_MEMBER = 1;
+
+    protected static final String[] PROJECTION_CONTACT = new String[] {
+        Contacts._ID,                           // 0
+        Contacts.DISPLAY_NAME_PRIMARY,          // 1
+        Contacts.DISPLAY_NAME_ALTERNATIVE,      // 2
+        Contacts.SORT_KEY_PRIMARY,              // 3
+        Contacts.STARRED,                       // 4
+        Contacts.CONTACT_PRESENCE,              // 5
+        Contacts.CONTACT_CHAT_CAPABILITY,       // 6
+        Contacts.PHOTO_ID,                      // 7
+        Contacts.PHOTO_THUMBNAIL_URI,           // 8
+        Contacts.LOOKUP_KEY,                    // 9
+        Contacts.PHONETIC_NAME,                 // 10
+        Contacts.HAS_PHONE_NUMBER,              // 11
+        Contacts.IS_USER_PROFILE,               // 12
+    };
+
+    protected static final int CONTACT_ID_COLUMN_INDEX = 0;
+    protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
+    protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
+    protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
+    protected static final int CONTACT_STARRED_COLUMN_INDEX = 4;
+    protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5;
+    protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6;
+    protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7;
+    protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8;
+    protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9;
+    protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10;
+    protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11;
+    protected static final int CONTACT_IS_USER_PROFILE = 12;
+
+    /**
+     * Modes that specify the status of the editor
+     */
+    public enum Status {
+        LOADING,    // Loader is fetching the data
+        EDITING,    // Not currently busy. We are waiting forthe user to enter data.
+        SAVING,     // Data is currently being saved
+        CLOSING     // Prevents any more saves
+    }
+
+    private Context mContext;
+    private String mAction;
+    private Uri mGroupUri;
+    private long mGroupId;
+    private Listener mListener;
+
+    private Status mStatus;
+
+    private View mRootView;
+    private ListView mListView;
+    private LayoutInflater mLayoutInflater;
+
+    private EditText mGroupNameView;
+    private ImageView mAccountIcon;
+    private TextView mAccountTypeTextView;
+    private TextView mAccountNameTextView;
+    private AutoCompleteTextView mAutoCompleteTextView;
+
+    private boolean mGroupNameIsReadOnly;
+    private String mAccountName;
+    private String mAccountType;
+    private String mOriginalGroupName = "";
+
+    private MemberListAdapter mMemberListAdapter;
+    private ContactPhotoManager mPhotoManager;
+
+    private Member mMemberToRemove;
+
+    private ContentResolver mContentResolver;
+    private SuggestedMemberListAdapter mAutoCompleteAdapter;
+
+    public GroupEditorFragment() {
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        setHasOptionsMenu(true);
+
+        mLayoutInflater = inflater;
+        mRootView = inflater.inflate(R.layout.group_editor_fragment, container, false);
+
+        mGroupNameView = (EditText) mRootView.findViewById(R.id.group_name);
+        mAccountIcon = (ImageView) mRootView.findViewById(R.id.account_icon);
+        mAccountTypeTextView = (TextView) mRootView.findViewById(R.id.account_type);
+        mAccountNameTextView = (TextView) mRootView.findViewById(R.id.account_name);
+        mAutoCompleteTextView = (AutoCompleteTextView) mRootView.findViewById(
+                R.id.add_member_field);
+
+        mListView = (ListView) mRootView.findViewById(android.R.id.list);
+        mListView.setAdapter(mMemberListAdapter);
+
+        return mRootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+        mPhotoManager = ContactPhotoManager.getInstance(mContext);
+        mMemberListAdapter = new MemberListAdapter();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // Edit an existing group
+        if (Intent.ACTION_EDIT.equals(mAction)) {
+            if (mListener != null) {
+                mListener.onTitleLoaded(R.string.editGroup_title_edit);
+            }
+            getLoaderManager().initLoader(LOADER_GROUP_METADATA, null,
+                    mGroupMetaDataLoaderListener);
+            getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null,
+                    mGroupMemberListLoaderListener);
+        } else if (Intent.ACTION_INSERT.equals(mAction)) {
+            if (mListener != null) {
+                mListener.onTitleLoaded(R.string.editGroup_title_insert);
+            }
+            setupAccountSwitcher();
+            mStatus = Status.EDITING;
+            // The user wants to create a new group, temporarily hide the "add members" text view
+            // TODO: Need to allow users to add members if it's a new group. Under the current
+            // approach, we can't add members because it needs a group ID in order to save,
+            // and we don't have a group ID for a new group until the whole group is saved.
+            mAutoCompleteTextView.setVisibility(View.GONE);
+        } else {
+            throw new IllegalArgumentException("Unknown Action String " + mAction +
+                    ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT);
+        }
+    }
+
+    public void setContentResolver(ContentResolver resolver) {
+        mContentResolver = resolver;
+        if (mAutoCompleteAdapter != null) {
+            mAutoCompleteAdapter.setContentResolver(mContentResolver);
+        }
+    }
+
+    /**
+     * Sets up the account header for a new group by taking the first account.
+     */
+    private void setupAccountSwitcher() {
+        // TODO: Allow switching between valid accounts
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+        final ArrayList<Account> accountsList = accountTypeManager.getAccounts(true);
+        if (accountsList.isEmpty()) {
+            return;
+        }
+        Account account = accountsList.get(0);
+
+        // Store account info for later
+        mAccountName = account.name;
+        mAccountType = account.type;
+
+        // Display account name
+        if (!TextUtils.isEmpty(mAccountName)) {
+            mAccountNameTextView.setText(
+                    mContext.getString(R.string.from_account_format, mAccountName));
+        }
+        // Display account type
+        final AccountType type = accountTypeManager.getAccountType(mAccountType);
+        mAccountTypeTextView.setText(type.getDisplayLabel(mContext));
+
+        // Display account icon
+        mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext));
+    }
+
+    /**
+     * Sets up the account header for an existing group.
+     */
+    private void setupAccountHeader() {
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
+        final AccountType accountType = accountTypes.getAccountType(mAccountType);
+        CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext);
+        if (!TextUtils.isEmpty(mAccountName)) {
+            mAccountNameTextView.setText(
+                    mContext.getString(R.string.from_account_format, mAccountName));
+        }
+        mAccountTypeTextView.setText(accountTypeDisplayLabel);
+        mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext));
+    }
+
+    public void load(String action, Uri groupUri) {
+        mAction = action;
+        mGroupUri = groupUri;
+        mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0;
+    }
+
+    private void bindGroupMetaData(Cursor cursor) {
+        if (cursor.getCount() == 0) {
+            if (mListener != null) {
+                mListener.onGroupNotFound();
+            }
+        }
+        try {
+            cursor.moveToFirst();
+            mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
+            mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
+            mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
+            mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1);
+        } catch (Exception e) {
+            Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now.");
+            if (mListener != null) {
+                mListener.onGroupNotFound();
+            }
+        } finally {
+            cursor.close();
+        }
+        // Setup the group metadata display (If the group name is ready only, don't let the user
+        // focus on the field).
+        mGroupNameView.setText(mOriginalGroupName);
+        mGroupNameView.setFocusable(!mGroupNameIsReadOnly);
+        setupAccountHeader();
+
+        // Setup the group member suggestion adapter
+        mAutoCompleteAdapter = new SuggestedMemberListAdapter(getActivity(),
+                android.R.layout.simple_dropdown_item_1line);
+        mAutoCompleteAdapter.setContentResolver(mContentResolver);
+        mAutoCompleteAdapter.setAccountType(mAccountType);
+        mAutoCompleteAdapter.setAccountName(mAccountName);
+        mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter);
+        mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                SuggestedMember member = mAutoCompleteAdapter.getItem(position);
+                loadMemberToAddToGroup(String.valueOf(member.getContactId()));
+
+                // Update the autocomplete adapter so the contact doesn't get suggested again
+                mAutoCompleteAdapter.addNewMember(member.getContactId());
+
+                // Clear out the text field
+                mAutoCompleteTextView.setText("");
+            }
+        });
+    }
+
+    public void loadMemberToAddToGroup(String contactId) {
+        Bundle args = new Bundle();
+        args.putString(MEMBER_LOOKUP_URI_KEY, contactId);
+        args.putInt(MEMBER_ACTION_KEY, ADD_MEMBER);
+        getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener);
+    }
+
+    private void loadMemberToRemoveFromGroup(String lookupUri) {
+        Bundle args = new Bundle();
+        args.putString(MEMBER_LOOKUP_URI_KEY, lookupUri);
+        args.putInt(MEMBER_ACTION_KEY, REMOVE_MEMBER);
+        getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args,
+                mDataLoaderListener);
+    }
+
+    public void finishAddMember(Uri lookupUri) {
+        Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast),
+                Toast.LENGTH_SHORT).show();
+        getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER);
+    }
+
+    public void finishRemoveMember(Uri lookupUri) {
+        Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast),
+                Toast.LENGTH_SHORT).show();
+        getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER);
+        mMemberListAdapter.removeMember(mMemberToRemove);
+    }
+
+    public void setListener(Listener value) {
+        mListener = value;
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+        inflater.inflate(R.menu.edit_group, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_done:
+                return save(SaveMode.CLOSE);
+            case R.id.menu_discard:
+                return revert();
+        }
+        return false;
+    }
+
+    private boolean revert() {
+        if (mGroupNameView.getText() != null &&
+                mGroupNameView.getText().toString().equals(mOriginalGroupName)) {
+            doRevertAction();
+        } else {
+            CancelEditDialogFragment.show(this);
+        }
+        return true;
+    }
+
+    private void doRevertAction() {
+        // When this Fragment is closed we don't want it to auto-save
+        mStatus = Status.CLOSING;
+        if (mListener != null) mListener.onReverted();
+    }
+
+    public static class CancelEditDialogFragment extends DialogFragment {
+
+        public static void show(GroupEditorFragment fragment) {
+            CancelEditDialogFragment dialog = new CancelEditDialogFragment();
+            dialog.setTargetFragment(fragment, 0);
+            dialog.show(fragment.getFragmentManager(), "cancelEditor");
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            AlertDialog dialog = new AlertDialog.Builder(getActivity())
+                    .setIconAttribute(android.R.attr.alertDialogIcon)
+                    .setTitle(R.string.cancel_confirmation_dialog_title)
+                    .setMessage(R.string.cancel_confirmation_dialog_message)
+                    .setPositiveButton(R.string.discard,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                ((GroupEditorFragment) getTargetFragment()).doRevertAction();
+                            }
+                        }
+                    )
+                    .setNegativeButton(android.R.string.cancel, null)
+                    .create();
+            return dialog;
+        }
+    }
+
+    /**
+     * Saves or creates the group based on the mode, and if successful
+     * finishes the activity. This actually only handles saving the group name.
+     * @return true when successful
+     */
+    public boolean save(int saveMode) {
+        if (!hasValidGroupName() || mStatus != Status.EDITING) {
+            return false;
+        }
+
+        // If we are about to close the editor - there is no need to refresh the data
+        if (saveMode == SaveMode.CLOSE) {
+            getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS);
+        }
+
+        mStatus = Status.SAVING;
+
+        if (!hasChanges()) {
+            onSaveCompleted(false, saveMode, mGroupUri);
+            return true;
+        }
+
+        Activity activity = getActivity();
+        // If the activity is not there anymore, then we can't continue with the save process.
+        if (activity == null) {
+            return false;
+        }
+        Intent saveIntent = null;
+        if (mAction == Intent.ACTION_INSERT) {
+            saveIntent = ContactSaveService.createNewGroupIntent(activity,
+                    new Account(mAccountName, mAccountType), mGroupNameView.getText().toString(),
+                    activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED);
+        } else if (mAction == Intent.ACTION_EDIT) {
+            saveIntent = ContactSaveService.createGroupRenameIntent(activity, mGroupId,
+                    mGroupNameView.getText().toString(), activity.getClass(),
+                    GroupEditorActivity.ACTION_SAVE_COMPLETED);
+        } else {
+            throw new IllegalStateException("Invalid intent action type " + mAction);
+        }
+        activity.startService(saveIntent);
+        return true;
+    }
+
+    public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) {
+        boolean success = groupUri != null;
+        Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")");
+        if (hadChanges) {
+            Toast.makeText(mContext, success ? R.string.groupSavedToast :
+                    R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
+        }
+        switch (saveMode) {
+            case SaveMode.CLOSE:
+            case SaveMode.HOME:
+                final Intent resultIntent;
+                final int resultCode;
+                if (success && groupUri != null) {
+                    final String requestAuthority =
+                            groupUri == null ? null : groupUri.getAuthority();
+
+                    resultIntent = new Intent();
+                    if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) {
+                        // Build legacy Uri when requested by caller
+                        final long groupId = ContentUris.parseId(groupUri);
+                        final Uri legacyContentUri = Uri.parse("content://contacts/groups");
+                        final Uri legacyUri = ContentUris.withAppendedId(
+                                legacyContentUri, groupId);
+                        resultIntent.setData(legacyUri);
+                    } else {
+                        // Otherwise pass back the given Uri
+                        resultIntent.setData(groupUri);
+                    }
+
+                    resultCode = Activity.RESULT_OK;
+                } else {
+                    resultCode = Activity.RESULT_CANCELED;
+                    resultIntent = null;
+                }
+                // It is already saved, so prevent that it is saved again
+                mStatus = Status.CLOSING;
+                if (mListener != null) {
+                    mListener.onSaveFinished(resultCode, resultIntent, saveMode == SaveMode.HOME);
+                }
+                break;
+            case SaveMode.RELOAD:
+                // TODO: Handle reloading the group list
+            default:
+                throw new IllegalStateException("Unsupported save mode " + saveMode);
+        }
+    }
+
+    private boolean hasValidGroupName() {
+        return !TextUtils.isEmpty(mGroupNameView.getText());
+    }
+
+    private boolean hasChanges() {
+        return mGroupNameView.getText() != null &&
+                !mGroupNameView.getText().toString().equals(mOriginalGroupName);
+    }
+
+    /**
+     * The listener for the group metadata (i.e. group name, account type, and account name) loader.
+     */
+    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener =
+            new LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            return new GroupMetaDataLoader(mContext, mGroupUri);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            mStatus = Status.EDITING;
+            bindGroupMetaData(data);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    };
+
+    /**
+     * The loader listener for the list of existing group members.
+     */
+    private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
+            new LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            return new GroupMemberLoader(mContext, mGroupId);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            List<Member> listMembers = new ArrayList<Member>();
+            List<Long> listContactIds = new ArrayList<Long>();
+            try {
+                data.moveToPosition(-1);
+                while (data.moveToNext()) {
+                    long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX);
+                    String lookupKey = data.getString(
+                            GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+                    String displayName = data.getString(
+                            GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
+                    String photoUri = data.getString(
+                            GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX);
+                    listMembers.add(new Member(lookupKey, contactId, displayName, photoUri));
+                    listContactIds.add(contactId);
+                }
+            } finally {
+                data.close();
+            }
+            // Update the list of displayed existing members
+            mMemberListAdapter.updateExistingMembersList(listMembers);
+            // Update the autocomplete adapter
+            mAutoCompleteAdapter.updateExistingMembersList(listContactIds);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    };
+
+    /**
+     * The listener to load a summary of details for a contact.
+     */
+    private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener =
+            new LoaderCallbacks<Cursor>() {
+
+        private int mMemberAction;
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            String memberId = args.getString(MEMBER_LOOKUP_URI_KEY);
+            mMemberAction = args.getInt(MEMBER_ACTION_KEY);
+            return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId),
+                    PROJECTION_CONTACT, null, null, null);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+            // Retrieve the contact data fields that will be sufficient to update the adapter with
+            // a new entry for this contact
+            Member member = null;
+            try {
+                cursor.moveToFirst();
+                long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+                String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
+                String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+                String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX);
+                getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER);
+                member = new Member(lookupKey, contactId, displayName, photoUri);
+            } finally {
+                cursor.close();
+            }
+
+            if (member == null) {
+                return;
+            }
+
+            // Don't do anything if the adapter already contains this member
+            // TODO: Come up with a better way to check membership using a DB query
+            if (mMemberListAdapter.contains(member)) {
+                Toast.makeText(getActivity(), getActivity().getString(
+                        R.string.contactAlreadyInGroup), Toast.LENGTH_SHORT).show();
+                return;
+            }
+
+            // Otherwise continue adding the member to list of members
+            mMemberListAdapter.addMember(member);
+
+            // Then start loading the full contact so that the change can be saved
+            // TODO: Combine these two loader steps into one. Either we get rid of the first loader
+            // (retrieving summary details) and just use the full contact loader, or find a way
+            // to save changes without loading the full contact
+            Bundle args = new Bundle();
+            args.putString(MEMBER_LOOKUP_URI_KEY, member.getLookupUri().toString());
+            args.putInt(MEMBER_ACTION_KEY, mMemberAction);
+            getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args,
+                    mDataLoaderListener);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    };
+
+    /**
+     * The listener for the loader that loads the full details of a contact so that when the data
+     * has arrived, the contact can be added or removed from the group.
+     */
+    private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener =
+            new LoaderCallbacks<ContactLoader.Result>() {
+
+        private int mMemberAction;
+
+        @Override
+        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+            mMemberAction = args.getInt(MEMBER_ACTION_KEY);
+            String memberLookupUri = args.getString(MEMBER_LOOKUP_URI_KEY);
+            return new ContactLoader(mContext, Uri.parse(memberLookupUri));
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+            if (data == ContactLoader.Result.NOT_FOUND || data == ContactLoader.Result.ERROR) {
+                Log.i(TAG, "Contact was not found");
+                return;
+            }
+            saveChange(data, mMemberAction);
+        }
+
+        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
+        }
+    };
+
+    private void saveChange(ContactLoader.Result data, int action) {
+        EntityDeltaList state = EntityDeltaList.fromIterator(data.getEntities().iterator());
+
+        // We need a raw contact to save this group membership change to, so find the first valid
+        // {@link EntityDelta}.
+        // TODO: Find a better way to do this. This will not work if the group is associated with
+        // the other
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
+        AccountType type = null;
+        EntityDelta entity = null;
+        int size = state.size();
+        for (int i = 0; i < size; i++) {
+            entity = state.get(i);
+            final ValuesDelta values = entity.getValues();
+            if (!values.isVisible()) continue;
+
+            final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            type = accountTypes.getAccountType(accountType);
+            // If the account name and type match this group's properties and the account type is
+            // not an external type, then use this raw contact
+            if (mAccountName.equals(accountName) && mAccountType.equals(accountType) &&
+                    !type.isExternal()) {
+                break;
+            }
+        }
+
+        Intent intent = null;
+        switch (action) {
+            case ADD_MEMBER:
+                DataKind groupMembershipKind = type.getKindForMimetype(
+                        GroupMembership.CONTENT_ITEM_TYPE);
+                ValuesDelta entry = EntityModifier.insertChild(entity, groupMembershipKind);
+                entry.put(GroupMembership.GROUP_ROW_ID, mGroupId);
+                // Form intent
+                intent = ContactSaveService.createSaveContactIntent(getActivity(), state,
+                        SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(),
+                        GroupEditorActivity.ACTION_ADD_MEMBER_COMPLETED);
+                break;
+            case REMOVE_MEMBER:
+                // TODO: Check that the contact was in the group in the first place
+                ArrayList<ValuesDelta> entries = entity.getMimeEntries(
+                        GroupMembership.CONTENT_ITEM_TYPE);
+                if (entries != null) {
+                    for (ValuesDelta valuesDeltaEntry : entries) {
+                        if (!valuesDeltaEntry.isDelete()) {
+                            Long groupId = valuesDeltaEntry.getAsLong(GroupMembership.GROUP_ROW_ID);
+                            if (groupId == mGroupId) {
+                                valuesDeltaEntry.markDeleted();
+                            }
+                        }
+                    }
+                }
+                intent = ContactSaveService.createSaveContactIntent(getActivity(), state,
+                        SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(),
+                        GroupEditorActivity.ACTION_REMOVE_MEMBER_COMPLETED);
+                break;
+            default:
+                throw new IllegalStateException("Invalid action for a group member " + action);
+        }
+        getActivity().startService(intent);
+    }
+
+    /**
+     * This represents a single member of the current group.
+     */
+    public static class Member {
+        private final Uri mLookupUri;
+        private final String mDisplayName;
+        private final Uri mPhotoUri;
+
+        public Member(String lookupKey, long contactId, String displayName, String photoUri) {
+            mLookupUri = Contacts.getLookupUri(contactId, lookupKey);
+            mDisplayName = displayName;
+            mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null;
+        }
+
+        public Uri getLookupUri() {
+            return mLookupUri;
+        }
+
+        public String getDisplayName() {
+            return mDisplayName;
+        }
+
+        public Uri getPhotoUri() {
+            return mPhotoUri;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (object instanceof Member) {
+                Member otherMember = (Member) object;
+                return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri());
+            }
+            return false;
+        }
+    }
+
+    /**
+     * This adapter displays a list of members for the current group being edited.
+     */
+    private final class MemberListAdapter extends BaseAdapter {
+
+        private List<Member> mNewMembersList = new ArrayList<Member>();
+        private List<Member> mTotalList = new ArrayList<Member>();
+
+        public boolean contains(Member member) {
+            return mTotalList.contains(member);
+        }
+
+        public void addMember(Member member) {
+            mNewMembersList.add(member);
+            mTotalList.add(member);
+            notifyDataSetChanged();
+        }
+
+        public void removeMember(Member member) {
+            if (mNewMembersList.contains(member)) {
+                mNewMembersList.remove(member);
+            }
+            mTotalList.remove(member);
+            notifyDataSetChanged();
+        }
+
+        public void updateExistingMembersList(List<Member> existingMembers) {
+            mTotalList.clear();
+            mTotalList.addAll(mNewMembersList);
+            mTotalList.addAll(existingMembers);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View result;
+            if (convertView == null) {
+                result = mLayoutInflater.inflate(R.layout.group_member_item, parent, false);
+            } else {
+                result = convertView;
+            }
+            final Member member = getItem(position);
+
+            QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge);
+            badge.assignContactUri(member.getLookupUri());
+
+            TextView name = (TextView) result.findViewById(R.id.name);
+            name.setText(member.getDisplayName());
+
+            View deleteButton = result.findViewById(R.id.delete_button_container);
+            deleteButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    loadMemberToRemoveFromGroup(member.getLookupUri().toString());
+                    // TODO: This is a hack to save the reference to the member that should be
+                    // removed. This won't work if the user tries to remove multiple times in a row
+                    // and reference is outdated. We actually need a hash map of member URIs to the
+                    // actual Member object. Before dealing with hash map though, hopefully we can
+                    // figure out how to batch save membership changes, which would eliminate the
+                    // need for this variable.
+                    mMemberToRemove = member;
+                }
+            });
+
+            mPhotoManager.loadPhoto(badge, member.getPhotoUri());
+            return result;
+        }
+
+        @Override
+        public int getCount() {
+            return mTotalList.size();
+        }
+
+        @Override
+        public Member getItem(int position) {
+            return mTotalList.get(position);
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return -1;
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/contacts/group/SuggestedMemberListAdapter.java b/src/com/android/contacts/group/SuggestedMemberListAdapter.java
new file mode 100644
index 0000000..dd74df5
--- /dev/null
+++ b/src/com/android/contacts/group/SuggestedMemberListAdapter.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2011 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.contacts.group;
+
+import com.android.contacts.R;
+import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.Filter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This adapter provides suggested contacts that can be added to a group for an
+ * {@link AutoCompleteTextView} within the group editor.
+ */
+public class SuggestedMemberListAdapter extends ArrayAdapter<SuggestedMember> {
+
+    private static final String[] PROJECTION_FILTERED_MEMBERS = new String[] {
+        RawContacts._ID,                        // 0
+        RawContacts.CONTACT_ID,                 // 1
+        RawContacts.DISPLAY_NAME_PRIMARY        // 2
+    };
+
+    private static final int RAW_CONTACT_ID_COLUMN_INDEX = 0;
+    private static final int CONTACT_ID_COLUMN_INDEX = 1;
+    private static final int DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 2;
+
+    private static final String[] PROJECTION_MEMBER_DATA = new String[] {
+        RawContacts._ID,                        // 0
+        RawContacts.CONTACT_ID,                 // 1
+        Data.MIMETYPE,                          // 2
+        Data.DATA1,                             // 3
+        Photo.PHOTO,                            // 4
+    };
+
+    private static final int MIMETYPE_COLUMN_INDEX = 2;
+    private static final int DATA_COLUMN_INDEX = 3;
+    private static final int PHOTO_COLUMN_INDEX = 4;
+
+    private Filter mFilter;
+    private ContentResolver mContentResolver;
+    private LayoutInflater mInflater;
+
+    private String mAccountType = "";
+    private String mAccountName = "";
+
+    private List<Long> mExistingMemberContactIds = new ArrayList<Long>();
+
+    private static final int SUGGESTIONS_LIMIT = 5;
+
+    public SuggestedMemberListAdapter(Context context, int textViewResourceId) {
+        super(context, textViewResourceId);
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    public void setAccountType(String accountType) {
+        mAccountType = accountType;
+    }
+
+    public void setAccountName(String accountName) {
+        mAccountName = accountName;
+    }
+
+    public void setContentResolver(ContentResolver resolver) {
+        mContentResolver = resolver;
+    }
+
+    public void updateExistingMembersList(List<Long> listContactIds) {
+        mExistingMemberContactIds = listContactIds;
+    }
+
+    public void addNewMember(long contactId) {
+        mExistingMemberContactIds.add(contactId);
+    }
+
+    public void removeMember(long contactId) {
+        if (mExistingMemberContactIds.contains(contactId)) {
+            mExistingMemberContactIds.remove(contactId);
+        }
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View result = convertView;
+        if (result == null) {
+            result = mInflater.inflate(R.layout.group_member_suggestion, parent, false);
+        }
+        // TODO: Use a viewholder
+        SuggestedMember member = getItem(position);
+        TextView text1 = (TextView) result.findViewById(R.id.text1);
+        TextView text2 = (TextView) result.findViewById(R.id.text2);
+        ImageView icon = (ImageView) result.findViewById(R.id.icon);
+        text1.setText(member.getDisplayName());
+        if (member.hasExtraInfo()) {
+            text2.setText(member.getExtraInfo());
+        } else {
+            text2.setVisibility(View.GONE);
+        }
+        byte[] byteArray = member.getPhotoByteArray();
+        if (byteArray == null) {
+            icon.setImageResource(R.drawable.ic_contact_picture);
+        }
+        else {
+            Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
+            icon.setImageBitmap(bitmap);
+        }
+        return result;
+    }
+
+    @Override
+    public Filter getFilter() {
+        if (mFilter == null) {
+            mFilter = new SuggestedMemberFilter();
+        }
+        return mFilter;
+    }
+
+    /**
+     * This filter queries for raw contacts that match the given account name and account type,
+     * as well as the search query.
+     */
+    public class SuggestedMemberFilter extends Filter {
+
+        @Override
+        protected FilterResults performFiltering(CharSequence prefix) {
+            FilterResults results = new FilterResults();
+            if (mContentResolver == null || TextUtils.isEmpty(prefix)) {
+                return results;
+            }
+
+            // Map of raw contact IDs to {@link SuggestedMember} objects
+            HashMap<Long, SuggestedMember> suggestionsMap = new HashMap<Long, SuggestedMember>();
+
+            // First query for all the raw contacts that match the given search query
+            // and have the same account name and type as specified in this adapter
+            String searchQuery = prefix.toString() + "%";
+            Cursor cursor = mContentResolver.query(
+                    RawContacts.CONTENT_URI, PROJECTION_FILTERED_MEMBERS,
+                    RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=? AND (" +
+                    RawContacts.DISPLAY_NAME_PRIMARY + " LIKE ? OR " +
+                    RawContacts.DISPLAY_NAME_ALTERNATIVE + " LIKE ? )",
+                    new String[] {mAccountName, mAccountType, searchQuery, searchQuery}, null);
+
+            if (cursor == null) {
+                return results;
+            }
+
+            // Read back the results from the cursor and filter out existing group members.
+            // For valid suggestions, add them to the hash map of suggested members.
+            try {
+                cursor.moveToPosition(-1);
+                while (cursor.moveToNext() && suggestionsMap.keySet().size() < SUGGESTIONS_LIMIT) {
+                    long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX);
+                    // Filter out contacts that have already been added to this group
+                    if (mExistingMemberContactIds.contains(contactId)) {
+                        continue;
+                    }
+                    // Otherwise, add the contact as a suggested new group member
+                    String displayName = cursor.getString(DISPLAY_NAME_PRIMARY_COLUMN_INDEX);
+                    long rawContactId = cursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX);
+                    suggestionsMap.put(rawContactId, new SuggestedMember(displayName, contactId));
+                }
+            } finally {
+                cursor.close();
+            }
+
+            int numSuggestions = suggestionsMap.keySet().size();
+            if (numSuggestions == 0) {
+                return results;
+            }
+
+            // Create a part of the selection string for the next query with the pattern (?, ?, ?)
+            // where the number of comma-separated question marks represent the number of raw
+            // contact IDs found in the previous query (while respective the SUGGESTION_LIMIT)
+            final StringBuilder rawContactIdSelectionBuilder = new StringBuilder();
+            final String[] questionMarks = new String[numSuggestions];
+            Arrays.fill(questionMarks, "?");
+            rawContactIdSelectionBuilder.append(RawContacts._ID + " IN (")
+                    .append(TextUtils.join(",", questionMarks))
+                    .append(")");
+
+            // Construct the selection args based on the raw contact IDs we're interested in
+            // (as well as the photo, email, and phone mimetypes)
+            List<String> selectionArgs = new ArrayList<String>();
+            selectionArgs.add(Photo.CONTENT_ITEM_TYPE);
+            selectionArgs.add(Email.CONTENT_ITEM_TYPE);
+            selectionArgs.add(Phone.CONTENT_ITEM_TYPE);
+            for (Long rawContactId : suggestionsMap.keySet()) {
+                selectionArgs.add(String.valueOf(rawContactId));
+            }
+
+            // Perform a second query to retrieve a photo and possibly a phone number or email
+            // address for the suggested contact
+            Cursor memberDataCursor = mContentResolver.query(
+                    RawContactsEntity.CONTENT_URI, PROJECTION_MEMBER_DATA,
+                    "(" + Data.MIMETYPE + "=? OR " + Data.MIMETYPE + "=? OR " + Data.MIMETYPE +
+                    "=?) AND " + rawContactIdSelectionBuilder.toString(),
+                    selectionArgs.toArray(new String[0]), null);
+
+            try {
+                memberDataCursor.moveToPosition(-1);
+                while (memberDataCursor.moveToNext()) {
+                    long rawContactId = memberDataCursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX);
+                    SuggestedMember member = suggestionsMap.get(rawContactId);
+                    if (member == null) {
+                        continue;
+                    }
+                    String mimetype = memberDataCursor.getString(MIMETYPE_COLUMN_INDEX);
+                    if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                        // Set photo
+                        byte[] bitmapArray = memberDataCursor.getBlob(PHOTO_COLUMN_INDEX);
+                        member.setPhotoByteArray(bitmapArray);
+                    } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype) ||
+                            Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                        // Set at most 1 extra piece of contact info that can be a phone number or
+                        // email
+                        if (!member.hasExtraInfo()) {
+                            String info = memberDataCursor.getString(DATA_COLUMN_INDEX);
+                            member.setExtraInfo(info);
+                        }
+                    }
+                }
+            } finally {
+                memberDataCursor.close();
+            }
+            results.values = suggestionsMap;
+            return results;
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+            HashMap<Long, SuggestedMember> map = (HashMap<Long, SuggestedMember>) results.values;
+            if (map == null || map.keySet() == null) {
+                return;
+            }
+
+            // Clear out the existing suggestions in this adapter
+            clear();
+
+            // Add all the suggested members to this adapter
+            for (SuggestedMember member : map.values()) {
+                add(member);
+            }
+
+            notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * This represents a single contact that is a suggestion for the user to add to a group.
+     */
+    public class SuggestedMember {
+
+        private long mContactId;
+        private String mDisplayName;
+        private String mExtraInfo;
+        private byte[] mPhoto;
+
+        public SuggestedMember(String displayName, long contactId) {
+            mDisplayName = displayName;
+            mContactId = contactId;
+        }
+
+        public String getDisplayName() {
+            return mDisplayName;
+        }
+
+        public String getExtraInfo() {
+            return mExtraInfo;
+        }
+
+        public long getContactId() {
+            return mContactId;
+        }
+
+        public byte[] getPhotoByteArray() {
+            return mPhoto;
+        }
+
+        public boolean hasExtraInfo() {
+            return mExtraInfo != null;
+        }
+
+        /**
+         * Set a phone number or email to distinguish this contact
+         */
+        public void setExtraInfo(String info) {
+            mExtraInfo = info;
+        }
+
+        public void setPhotoByteArray(byte[] photo) {
+            mPhoto = photo;
+        }
+    }
+}
diff --git a/src/com/android/contacts/interactions/GroupRenamingDialogFragment.java b/src/com/android/contacts/interactions/GroupRenamingDialogFragment.java
deleted file mode 100644
index 0db435e..0000000
--- a/src/com/android/contacts/interactions/GroupRenamingDialogFragment.java
+++ /dev/null
@@ -1,64 +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.contacts.interactions;
-
-import com.android.contacts.ContactSaveService;
-import com.android.contacts.R;
-
-import android.app.FragmentManager;
-import android.os.Bundle;
-import android.widget.EditText;
-
-/**
- * A dialog for renaming a group.
- */
-public class GroupRenamingDialogFragment extends GroupNameDialogFragment {
-
-    private static final String ARG_GROUP_ID = "groupId";
-    private static final String ARG_LABEL = "label";
-
-    public static void show(FragmentManager fragmentManager, long groupId, String label) {
-        GroupRenamingDialogFragment dialog = new GroupRenamingDialogFragment();
-        Bundle args = new Bundle();
-        args.putLong(ARG_GROUP_ID, groupId);
-        args.putString(ARG_LABEL, label);
-        dialog.setArguments(args);
-        dialog.show(fragmentManager, "renameGroup");
-    }
-
-    @Override
-    protected void initializeGroupLabelEditText(EditText editText) {
-        String label = getArguments().getString(ARG_LABEL);
-        editText.setText(label);
-        if (label != null) {
-            editText.setSelection(label.length());
-        }
-    }
-
-    @Override
-    protected int getTitleResourceId() {
-        return R.string.rename_group_dialog_title;
-    }
-
-    @Override
-    protected void onCompleted(String groupLabel) {
-        Bundle arguments = getArguments();
-        long groupId = arguments.getLong(ARG_GROUP_ID);
-
-        getActivity().startService(ContactSaveService.createGroupRenameIntent(
-                getActivity(), groupId, groupLabel));
-    }
-}