Adding groups to contact editor

Change-Id: I724b075506ed6949d92c319d2155e2896ee89d6e
diff --git a/res/layout-xlarge/item_group_membership.xml b/res/layout-xlarge/item_group_membership.xml
new file mode 100644
index 0000000..8ac514d
--- /dev/null
+++ b/res/layout-xlarge/item_group_membership.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- the body surrounding all editors for a specific kind -->
+
+<com.android.contacts.ui.widget.GroupMembershipView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/kind_title"
+        android:layout_width="100dip"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:singleLine="true"
+        android:ellipsize="marquee" />
+
+    <Button
+        android:id="@+id/group_list"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:gravity="left"
+        android:ellipsize="end"
+    />
+</com.android.contacts.ui.widget.GroupMembershipView>
diff --git a/res/layout/item_group_membership.xml b/res/layout/item_group_membership.xml
new file mode 100644
index 0000000..f9afdf9
--- /dev/null
+++ b/res/layout/item_group_membership.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<com.android.contacts.ui.widget.GroupMembershipView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="vertical">
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:background="?android:attr/listDivider" />
+
+    <LinearLayout
+        android:id="@+id/kind_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_marginLeft="14dip"
+        android:layout_marginTop="2dip"
+        android:layout_marginBottom="2dip"
+        android:layout_marginRight="?android:attr/scrollbarSize"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:focusable="true"
+        android:clickable="true">
+
+        <TextView
+            android:id="@+id/kind_title"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="@color/kind_title"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal" />
+
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/group_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:ellipsize="end"
+        android:gravity="left"
+    />
+
+</com.android.contacts.ui.widget.GroupMembershipView>
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index e353d70..f382d2c 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -697,6 +697,11 @@
             mAfter.put(key, value);
         }
 
+        public void put(String key, long value) {
+            ensureUpdate();
+            mAfter.put(key, value);
+        }
+
         public void putNull(String key) {
             ensureUpdate();
             mAfter.putNull(key);
diff --git a/src/com/android/contacts/model/ExchangeSource.java b/src/com/android/contacts/model/ExchangeSource.java
index 3f2ab6c..9fb5f5b 100644
--- a/src/com/android/contacts/model/ExchangeSource.java
+++ b/src/com/android/contacts/model/ExchangeSource.java
@@ -57,6 +57,7 @@
         inflatePhoto(context, inflateLevel);
         inflateNote(context, inflateLevel);
         inflateWebsite(context, inflateLevel);
+        inflateGroupMembership(context, inflateLevel);
 
         setInflatedLevel(inflateLevel);
     }
diff --git a/src/com/android/contacts/model/FallbackSource.java b/src/com/android/contacts/model/FallbackSource.java
index 78bc1a4..364bc50 100644
--- a/src/com/android/contacts/model/FallbackSource.java
+++ b/src/com/android/contacts/model/FallbackSource.java
@@ -26,6 +26,7 @@
 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Note;
@@ -82,6 +83,7 @@
         inflateWebsite(context, inflateLevel);
         inflateEvent(context, inflateLevel);
         inflateSipAddress(context, inflateLevel);
+        inflateGroupMembership(context, inflateLevel);
 
         setInflatedLevel(inflateLevel);
 
@@ -446,6 +448,23 @@
         return kind;
     }
 
+    protected DataKind inflateGroupMembership(Context context, int inflateLevel) {
+        DataKind kind = getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
+                    R.string.groupsLabel, android.R.drawable.sym_contact_card, 999, true));
+
+            kind.isList = false;
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+        }
+
+        return kind;
+    }
+
     /**
      * Simple inflater that assumes a string resource has a "%s" that will be
      * filled from the given column.
diff --git a/src/com/android/contacts/model/GoogleSource.java b/src/com/android/contacts/model/GoogleSource.java
index 6d4631f..fc290c6 100644
--- a/src/com/android/contacts/model/GoogleSource.java
+++ b/src/com/android/contacts/model/GoogleSource.java
@@ -51,8 +51,7 @@
         inflateWebsite(context, inflateLevel);
         inflateEvent(context, inflateLevel);
         inflateSipAddress(context, inflateLevel);
-
-        // TODO: GOOGLE: GROUPMEMBERSHIP
+        inflateGroupMembership(context, inflateLevel);
 
         setInflatedLevel(inflateLevel);
 
diff --git a/src/com/android/contacts/ui/widget/BaseContactEditorView.java b/src/com/android/contacts/ui/widget/BaseContactEditorView.java
index 7ee2dac..806584e 100644
--- a/src/com/android/contacts/ui/widget/BaseContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/BaseContactEditorView.java
@@ -17,19 +17,19 @@
 package com.android.contacts.ui.widget;
 
 import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource.EditType;
-import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityModifier;
 import com.android.contacts.ui.ViewIdGenerator;
 
 import android.content.Context;
 import android.content.Entity;
+import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.widget.LinearLayout;
@@ -59,6 +59,9 @@
         super(context, attrs);
     }
 
+    public void setGroupMetaData(Cursor groupMetaData) {
+    }
+
     /**
      * Assign the given {@link Bitmap} to the internal {@link PhotoEditorView}
      * for the {@link EntityDelta} currently being edited.
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 497e3a7..dfa0f69 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -27,6 +27,8 @@
 
 import android.content.Context;
 import android.content.Entity;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
@@ -60,6 +62,7 @@
 public class ContactEditorView extends BaseContactEditorView {
     private View mPhotoStub;
     private GenericEditorView mName;
+    private GroupMembershipView mGroupMembershipView;
 
     private ViewGroup mFields;
 
@@ -166,6 +169,13 @@
         mFields.setVisibility(View.VISIBLE);
         mName.setVisibility(View.VISIBLE);
 
+        DataKind groupMembershipKind = source.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
+        if (groupMembershipKind != null) {
+            mGroupMembershipView = (GroupMembershipView)mInflater.inflate(
+                    R.layout.item_group_membership, mFields, false);
+            mGroupMembershipView.setKind(groupMembershipKind);
+        }
+
         // Create editor sections for each possible data kind
         for (DataKind kind : source.getSortedDataKinds()) {
             // Skip kind of not editable
@@ -181,6 +191,10 @@
                 final ValuesDelta primary = state.getPrimaryEntry(mimeType);
                 mPhoto.setValues(kind, primary, state, false, vig);
                 mPhotoStub.setVisibility(View.VISIBLE);
+            } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                if (mGroupMembershipView != null) {
+                    mGroupMembershipView.setState(state);
+                }
             } else {
                 // Otherwise use generic section-based editors
                 if (kind.fieldList == null) continue;
@@ -190,6 +204,17 @@
                 mFields.addView(section);
             }
         }
+
+        if (mGroupMembershipView != null) {
+            mFields.addView(mGroupMembershipView);
+        }
+    }
+
+    @Override
+    public void setGroupMetaData(Cursor groupMetaData) {
+        if (mGroupMembershipView != null) {
+            mGroupMembershipView.setGroupMetaData(groupMetaData);
+        }
     }
 
     public GenericEditorView getNameEditor() {
diff --git a/src/com/android/contacts/ui/widget/GroupMembershipView.java b/src/com/android/contacts/ui/widget/GroupMembershipView.java
new file mode 100644
index 0000000..88d9f1e
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/GroupMembershipView.java
@@ -0,0 +1,262 @@
+/*
+ * 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.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.views.GroupMetaDataLoader;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.RawContacts;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListPopupWindow;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+/**
+ * An editor for group membership.  Displays the current group membership list and
+ * brings up a dialog to change it.
+ */
+public class GroupMembershipView extends LinearLayout
+        implements OnClickListener, OnItemClickListener {
+
+    public static final class GroupSelectionItem {
+        private final long mGroupId;
+        private final String mTitle;
+        private boolean mChecked;
+
+        public GroupSelectionItem(long groupId, String title, boolean checked) {
+            this.mGroupId = groupId;
+            this.mTitle = title;
+            mChecked = checked;
+        }
+
+        public long getGroupId() {
+            return mGroupId;
+        }
+
+        public boolean isChecked() {
+            return mChecked;
+        }
+
+        public void setChecked(boolean checked) {
+            mChecked = checked;
+        }
+
+        @Override
+        public String toString() {
+            return mTitle;
+        }
+    }
+
+    private EntityDelta mState;
+    private Cursor mGroupMetaData;
+    private String mAccountName;
+    private String mAccountType;
+    private TextView mGroupList;
+    private ArrayAdapter<GroupSelectionItem> mAdapter;
+    private long mFavoritesGroupId;
+    private ListPopupWindow mPopup;
+    private DataKind mKind;
+
+    public GroupMembershipView(Context context) {
+        super(context);
+    }
+
+    public GroupMembershipView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setKind(DataKind kind) {
+        mKind = kind;
+        TextView kindTitle = (TextView) findViewById(R.id.kind_title);
+        kindTitle.setText(kind.titleRes);
+    }
+
+    public void setGroupMetaData(Cursor groupMetaData) {
+        this.mGroupMetaData = groupMetaData;
+        updateView();
+    }
+
+    public void setState(EntityDelta state) {
+        mState = state;
+        mAccountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+        mAccountName = state.getValues().getAsString(RawContacts.ACCOUNT_NAME);
+        updateView();
+    }
+
+    private void updateView() {
+        if (mGroupMetaData == null || mAccountType == null || mAccountName == null) {
+            setVisibility(GONE);
+            return;
+        }
+
+        boolean accountHasGroups = false;
+        StringBuilder sb = new StringBuilder();
+        mGroupMetaData.moveToPosition(-1);
+        while (mGroupMetaData.moveToNext()) {
+            String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
+            String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
+            if (accountName.equals(mAccountName) && accountType.equals(mAccountType)) {
+                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
+                if (!mGroupMetaData.isNull(GroupMetaDataLoader.FAVORITES)
+                        && mGroupMetaData.getInt(GroupMetaDataLoader.FAVORITES) != 0) {
+                    mFavoritesGroupId = groupId;
+                } else {
+                    accountHasGroups = true;
+                }
+
+                // Exclude favorites from the list - they are handled with special UI (star)
+                if (groupId != mFavoritesGroupId && hasMembership(groupId)) {
+                    String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
+                    if (sb.length() != 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(title);
+                }
+            }
+        }
+
+        if (!accountHasGroups) {
+            setVisibility(GONE);
+            return;
+        }
+
+        if (mGroupList == null) {
+            mGroupList = (TextView) findViewById(R.id.group_list);
+            mGroupList.setOnClickListener(this);
+        }
+
+        mGroupList.setText(sb);
+        setVisibility(VISIBLE);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mPopup != null && mPopup.isShowing()) {
+            mPopup.dismiss();
+            return;
+        }
+
+        mAdapter = new ArrayAdapter<GroupSelectionItem>(
+                getContext(), android.R.layout.simple_list_item_multiple_choice);
+
+        mGroupMetaData.moveToPosition(-1);
+        while (mGroupMetaData.moveToNext()) {
+            String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
+            String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
+            if (accountName.equals(mAccountName) && accountType.equals(mAccountType)) {
+                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
+                if (groupId != mFavoritesGroupId) {
+                    String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
+                    boolean checked = hasMembership(groupId);
+                    mAdapter.add(new GroupSelectionItem(groupId, title, checked));
+                }
+            }
+        }
+
+        mPopup = new ListPopupWindow(getContext());
+        mPopup.setAnchorView(mGroupList);
+        mPopup.setAdapter(mAdapter);
+        mPopup.show();
+
+        ListView listView = mPopup.getListView();
+        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+        int count = mAdapter.getCount();
+        for (int i = 0; i < count; i++) {
+            listView.setItemChecked(i, mAdapter.getItem(i).isChecked());
+        }
+
+        listView.setOnItemClickListener(this);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mPopup != null) {
+            mPopup.dismiss();
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        ListView list = (ListView) parent;
+        int count = mAdapter.getCount();
+        for (int i = 0; i < count; i++) {
+            mAdapter.getItem(i).setChecked(list.isItemChecked(i));
+        }
+
+        // First remove the memberships that have been unchecked
+        ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
+        if (entries != null) {
+            for (ValuesDelta entry : entries) {
+                long groupId = entry.getAsLong(GroupMembership.GROUP_ROW_ID);
+                if (groupId != mFavoritesGroupId && !isGroupChecked(groupId)) {
+                    entry.markDeleted();
+                }
+            }
+        }
+
+        // Now add the newly selected items
+        for (int i = 0; i < count; i++) {
+            GroupSelectionItem item = mAdapter.getItem(i);
+            long groupId = item.getGroupId();
+            if (item.isChecked() && !hasMembership(groupId)) {
+                ValuesDelta entry = EntityModifier.insertChild(mState, mKind);
+                entry.put(GroupMembership.GROUP_ROW_ID, groupId);
+            }
+        }
+
+        updateView();
+    }
+
+    private boolean isGroupChecked(long groupId) {
+        int count = mAdapter.getCount();
+        for (int i = 0; i < count; i++) {
+            GroupSelectionItem item = mAdapter.getItem(i);
+            if (groupId == item.getGroupId()) {
+                return item.isChecked();
+            }
+        }
+        return false;
+    }
+
+    private boolean hasMembership(long groupId) {
+        ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
+        if (entries != null) {
+            for (ValuesDelta values : entries) {
+                if (values.getAsLong(GroupMembership.GROUP_ROW_ID) == groupId) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/views/ContactLoader.java b/src/com/android/contacts/views/ContactLoader.java
index c187adc..d3b876a 100644
--- a/src/com/android/contacts/views/ContactLoader.java
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -53,10 +53,12 @@
     private static final String TAG = "ContactLoader";
 
     private Uri mLookupUri;
+    private boolean mLoadGroupMetaData;
     private Result mContact;
     private ForceLoadContentObserver mObserver;
     private boolean mDestroyed;
 
+
     public interface Listener {
         public void onContactLoaded(Result contact);
     }
@@ -102,7 +104,7 @@
         private String mDirectoryAccountName;
         private int mDirectoryExportSupport;
 
-        private ArrayList<Group> mGroups;
+        private ArrayList<GroupMetaData> mGroups;
 
         /**
          * Constructor for case "no contact found". This must only be used for the
@@ -270,63 +272,18 @@
             return result;
         }
 
-        public void addGroupMetaData(Group group) {
+        public void addGroupMetaData(GroupMetaData group) {
             if (mGroups == null) {
-                mGroups = new ArrayList<Group>();
+                mGroups = new ArrayList<GroupMetaData>();
             }
             mGroups.add(group);
         }
 
-        public List<Group> getGroupMetaData() {
+        public List<GroupMetaData> getGroupMetaData() {
             return mGroups;
         }
     }
 
-    /**
-     * Meta-data for a contact group.  We load all groups associated with the contact's
-     * constituent accounts.
-     */
-    public static final class Group {
-        private String mAccountName;
-        private String mAccountType;
-        private long mGroupId;
-        private String mTitle;
-        private boolean mDefaultGroup;
-        private boolean mFavorites;
-
-        public Group(String accountName, String accountType, long groupId, String title,
-                boolean defaultGroup, boolean favorites) {
-            this.mGroupId = groupId;
-            this.mTitle = title;
-            this.mDefaultGroup = defaultGroup;
-            this.mFavorites = favorites;
-        }
-
-        public String getAccountName() {
-            return mAccountName;
-        }
-
-        public String getAccountType() {
-            return mAccountType;
-        }
-
-        public long getGroupId() {
-            return mGroupId;
-        }
-
-        public String getTitle() {
-            return mTitle;
-        }
-
-        public boolean isDefaultGroup() {
-            return mDefaultGroup;
-        }
-
-        public boolean isFavorites() {
-            return mFavorites;
-        }
-    }
-
     private static class ContactQuery {
         // Projection used for the query that loads all data for the entire contact.
         final static String[] COLUMNS = new String[] {
@@ -503,7 +460,7 @@
                 Result result = loadContactEntity(resolver, uriCurrentFormat);
                 if (result.isDirectoryEntry()) {
                     loadDirectoryMetaData(result);
-                } else {
+                } else if (mLoadGroupMetaData) {
                     loadGroupMetaData(result);
                 }
                 return result;
@@ -795,7 +752,7 @@
                             ? false
                             : cursor.getInt(GroupQuery.FAVORITES) != 0;
 
-                    result.addGroupMetaData(new Group(
+                    result.addGroupMetaData(new GroupMetaData(
                             accountName, accountType, groupId, title, defaultGroup, favorites));
                 }
             } finally {
@@ -836,8 +793,13 @@
     }
 
     public ContactLoader(Context context, Uri lookupUri) {
+        this(context, lookupUri, false);
+    }
+
+    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData) {
         super(context);
         mLookupUri = lookupUri;
+        mLoadGroupMetaData = loadGroupMetaData;
     }
 
     @Override
diff --git a/src/com/android/contacts/views/GroupMetaData.java b/src/com/android/contacts/views/GroupMetaData.java
new file mode 100644
index 0000000..dd52eb0
--- /dev/null
+++ b/src/com/android/contacts/views/GroupMetaData.java
@@ -0,0 +1,61 @@
+/*
+ * 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.views;
+
+/**
+ * Meta-data for a contact group.  We load all groups associated with the contact's
+ * constituent accounts.
+ */
+public final class GroupMetaData {
+    private String mAccountName;
+    private String mAccountType;
+    private long mGroupId;
+    private String mTitle;
+    private boolean mDefaultGroup;
+    private boolean mFavorites;
+
+    public GroupMetaData(String accountName, String accountType, long groupId, String title,
+            boolean defaultGroup, boolean favorites) {
+        this.mGroupId = groupId;
+        this.mTitle = title;
+        this.mDefaultGroup = defaultGroup;
+        this.mFavorites = favorites;
+    }
+
+    public String getAccountName() {
+        return mAccountName;
+    }
+
+    public String getAccountType() {
+        return mAccountType;
+    }
+
+    public long getGroupId() {
+        return mGroupId;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public boolean isDefaultGroup() {
+        return mDefaultGroup;
+    }
+
+    public boolean isFavorites() {
+        return mFavorites;
+    }
+}
diff --git a/src/com/android/contacts/views/GroupMetaDataLoader.java b/src/com/android/contacts/views/GroupMetaDataLoader.java
new file mode 100644
index 0000000..adfe0bf
--- /dev/null
+++ b/src/com/android/contacts/views/GroupMetaDataLoader.java
@@ -0,0 +1,46 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.provider.ContactsContract.Groups;
+
+/**
+ * Group meta-data loader.  Loads all groups from the database.
+ */
+public final class GroupMetaDataLoader extends CursorLoader {
+
+    private final static String[] COLUMNS = new String[] {
+        Groups.ACCOUNT_NAME,
+        Groups.ACCOUNT_TYPE,
+        Groups._ID,
+        Groups.TITLE,
+        Groups.AUTO_ADD,
+        Groups.FAVORITES,
+    };
+
+    public final static int ACCOUNT_NAME = 0;
+    public final static int ACCOUNT_TYPE = 1;
+    public final static int GROUP_ID = 2;
+    public final static int TITLE = 3;
+    public final static int AUTO_ADD = 4;
+    public final static int FAVORITES = 5;
+
+    public GroupMetaDataLoader(Context context) {
+        super(context, Groups.CONTENT_URI, COLUMNS, null, null, null);
+    }
+}
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index 9e968e3..5e8ab8c 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -32,7 +32,7 @@
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.views.ContactLoader;
-import com.android.contacts.views.ContactLoader.Group;
+import com.android.contacts.views.GroupMetaData;
 import com.android.contacts.views.editor.SelectAccountDialogFragment;
 import com.android.internal.telephony.ITelephony;
 
@@ -166,8 +166,8 @@
         mSections.add(mPostalEntries);
         mSections.add(mNicknameEntries);
         mSections.add(mOrganizationEntries);
-        mSections.add(mGroupEntries);
         mSections.add(mOtherEntries);
+        mSections.add(mGroupEntries);
     }
 
     @Override
@@ -305,8 +305,6 @@
 
                 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
                     Long groupId = entryValues.getAsLong(GroupMembership.GROUP_ROW_ID);
-                    System.out.println("MEMbERSHIP: " +
-                            groupId);
                     if (groupId != null) {
                         handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId);
                     }
@@ -512,12 +510,12 @@
      * Ignores default groups (e.g. My Contacts) and favorites groups.
      */
     private void handleGroupMembership(
-            ArrayList<String> groups, List<Group> groupMetaData, long groupId) {
+            ArrayList<String> groups, List<GroupMetaData> groupMetaData, long groupId) {
         if (groupMetaData == null) {
             return;
         }
 
-        for (Group group : groupMetaData) {
+        for (GroupMetaData group : groupMetaData) {
             if (group.getGroupId() == groupId) {
                 if (!group.isDefaultGroup() && !group.isFavorites()) {
                     String title = group.getTitle();
@@ -1167,7 +1165,7 @@
             new LoaderCallbacks<ContactLoader.Result>() {
         @Override
         public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
-            return new ContactLoader(mContext, mLookupUri);
+            return new ContactLoader(mContext, mLookupUri, true /* loadGroupMetaData */);
         }
 
         @Override
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
index 4070ca9..51d963a 100644
--- a/src/com/android/contacts/views/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -36,6 +36,7 @@
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
 import com.android.contacts.views.ContactLoader;
+import com.android.contacts.views.GroupMetaDataLoader;
 import com.android.contacts.views.editor.AggregationSuggestionEngine.Suggestion;
 import com.google.android.collect.Lists;
 
@@ -53,11 +54,13 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.CursorLoader;
 import android.content.Entity;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.OperationApplicationException;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.media.MediaScannerConnection;
@@ -72,6 +75,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.MediaStore;
 import android.text.TextUtils;
@@ -105,6 +109,7 @@
     private static final String TAG = "ContactEditorFragment";
 
     private static final int LOADER_DATA = 1;
+    private static final int LOADER_GROUPS = 2;
 
     private static final String KEY_URI = "uri";
     private static final String KEY_ACTION = "action";
@@ -178,6 +183,8 @@
     private static final File PHOTO_DIR = new File(
             Environment.getExternalStorageDirectory() + "/DCIM/Camera");
 
+    private Cursor mGroupMetaData;
+
     /**
      * A delay in milliseconds used for bringing aggregation suggestions to
      * the visible part of the screen. The reason this has to be done after
@@ -272,6 +279,12 @@
         }
     }
 
+    @Override
+    public void onStart() {
+        getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
+        super.onStart();
+    }
+
     public void load(String action, Uri lookupUri, String mimeType, Bundle intentExtras) {
         mAction = action;
         mLookupUri = lookupUri;
@@ -492,6 +505,8 @@
             }
         }
 
+        bindGroupMetaData();
+
         // Show editor now that we've loaded state
         mContent.setVisibility(View.VISIBLE);
 
@@ -501,6 +516,18 @@
         if (activity != null) activity.invalidateOptionsMenu();
     }
 
+    private void bindGroupMetaData() {
+        if (mGroupMetaData == null) {
+            return;
+        }
+
+        int editorCount = mContent.getChildCount();
+        for (int i = 0; i < editorCount; i++) {
+            BaseContactEditorView editor = (BaseContactEditorView) mContent.getChildAt(i);
+            editor.setGroupMetaData(mGroupMetaData);
+        }
+    }
+
     @Override
     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
         inflater.inflate(R.menu.edit, menu);
@@ -1506,6 +1533,24 @@
         }
     };
 
+    /**
+     * The listener for the group meta data loader
+     */
+    private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
+            new LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            return new GroupMetaDataLoader(mContext);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            mGroupMetaData = data;
+            bindGroupMetaData();
+        }
+    };
+
     @Override
     public void onSplitContactConfirmed() {
         mState.markRawContactsForSplitting();