Adding groups to contact view

Change-Id: I2bb256e8ff220538563b2d3ed777ac62d070afca
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 30134a9..0a88e78 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -925,6 +925,8 @@
     <string name="websiteLabelsGroup">Website</string>
     <!-- Header that expands to list all event types when editing an event of a contact -->
     <string name="eventLabelsGroup">Event</string>
+    <!-- Header for the list of all groups for a contact -->
+    <string name="groupsLabel">Groups</string>
 
     <!-- Single-character overlay for home phone numbers when creating desktop shortcuts -->
     <string name="type_short_home">H</string>
diff --git a/src/com/android/contacts/views/ContactLoader.java b/src/com/android/contacts/views/ContactLoader.java
index 19a28b5..c187adc 100644
--- a/src/com/android/contacts/views/ContactLoader.java
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -17,7 +17,6 @@
 package com.android.contacts.views;
 
 import com.android.contacts.util.DataStatus;
-import com.android.contacts.views.ContactLoader.Result;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -29,7 +28,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -39,6 +37,7 @@
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.Log;
@@ -103,6 +102,8 @@
         private String mDirectoryAccountName;
         private int mDirectoryExportSupport;
 
+        private ArrayList<Group> mGroups;
+
         /**
          * Constructor for case "no contact found". This must only be used for the
          * final {@link Result#NOT_FOUND} singleton
@@ -268,6 +269,62 @@
             }
             return result;
         }
+
+        public void addGroupMetaData(Group group) {
+            if (mGroups == null) {
+                mGroups = new ArrayList<Group>();
+            }
+            mGroups.add(group);
+        }
+
+        public List<Group> 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 {
@@ -418,6 +475,24 @@
         public final static int EXPORT_SUPPORT = 5;
     }
 
+    private static class GroupQuery {
+        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 ID = 2;
+        public final static int TITLE = 3;
+        public final static int AUTO_ADD = 4;
+        public final static int FAVORITES = 5;
+    }
+
     private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
 
         @Override
@@ -428,6 +503,8 @@
                 Result result = loadContactEntity(resolver, uriCurrentFormat);
                 if (result.isDirectoryEntry()) {
                     loadDirectoryMetaData(result);
+                } else {
+                    loadGroupMetaData(result);
                 }
                 return result;
             } catch (Exception e) {
@@ -681,6 +758,51 @@
             }
         }
 
+        /**
+         * Loads groups meta-data for all groups associated with all constituent raw contacts'
+         * accounts.
+         */
+        private void loadGroupMetaData(Result result) {
+            StringBuilder selection = new StringBuilder();
+            ArrayList<String> selectionArgs = new ArrayList<String>();
+            for (Entity entity : result.mEntities) {
+                ContentValues values = entity.getEntityValues();
+                String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+                String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+                if (accountName != null && accountType != null) {
+                    if (selection.length() != 0) {
+                        selection.append(" OR ");
+                    }
+                    selection.append(
+                            "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?)");
+                    selectionArgs.add(accountName);
+                    selectionArgs.add(accountType);
+                }
+            }
+            Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
+                    GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
+                    null);
+            try {
+                while (cursor.moveToNext()) {
+                    final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+                    final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+                    final long groupId = cursor.getLong(GroupQuery.ID);
+                    final String title = cursor.getString(GroupQuery.TITLE);
+                    final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
+                            ? false
+                            : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+                    final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
+                            ? false
+                            : cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+                    result.addGroupMetaData(new Group(
+                            accountName, accountType, groupId, title, defaultGroup, favorites));
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
         @Override
         protected void onPostExecute(Result result) {
             // The creator isn't interested in any further updates
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index 2bb5e52..9e968e3 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -32,6 +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.editor.SelectAccountDialogFragment;
 import com.android.internal.telephony.ITelephony;
 
@@ -58,6 +59,7 @@
 import android.os.ServiceManager;
 import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Note;
@@ -96,6 +98,8 @@
 import android.widget.Toast;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public class ContactDetailFragment extends Fragment implements OnCreateContextMenuListener,
         OnItemClickListener, SelectAccountDialogFragment.Listener {
@@ -271,6 +275,7 @@
             return;
         }
 
+        ArrayList<String> groups = new ArrayList<String>();
         for (Entity entity: mContactData.getEntities()) {
             final ContentValues entValues = entity.getEntityValues();
             final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
@@ -290,7 +295,6 @@
                 mWritableRawContactIds.add(rawContactId);
             }
 
-
             for (NamedContentValues subValue : entity.getSubValues()) {
                 final ContentValues entryValues = subValue.values;
                 entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
@@ -299,6 +303,16 @@
                 final String mimeType = entryValues.getAsString(Data.MIMETYPE);
                 if (mimeType == null) continue;
 
+                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);
+                    }
+                    continue;
+                }
+
                 final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
                         ContactsSource.LEVEL_CONSTRAINTS);
                 if (kind == null) continue;
@@ -474,6 +488,46 @@
                 }
             }
         }
+
+        if (!groups.isEmpty()) {
+            ViewEntry entry = new ViewEntry();
+            Collections.sort(groups);
+            StringBuilder sb = new StringBuilder();
+            int size = groups.size();
+            for (int i = 0; i < size; i++) {
+                if (i != 0) {
+                    sb.append(", ");
+                }
+                sb.append(groups.get(i));
+            }
+            entry.mimetype = GroupMembership.MIMETYPE;
+            entry.kind = mContext.getString(R.string.groupsLabel);
+            entry.data = sb.toString();
+            mGroupEntries.add(entry);
+        }
+    }
+
+    /**
+     * Maps group ID to the corresponding group name, collapses all synonymous groups.
+     * Ignores default groups (e.g. My Contacts) and favorites groups.
+     */
+    private void handleGroupMembership(
+            ArrayList<String> groups, List<Group> groupMetaData, long groupId) {
+        if (groupMetaData == null) {
+            return;
+        }
+
+        for (Group group : groupMetaData) {
+            if (group.getGroupId() == groupId) {
+                if (!group.isDefaultGroup() && !group.isFavorites()) {
+                    String title = group.getTitle();
+                    if (!groups.contains(title)) {
+                        groups.add(title);
+                    }
+                }
+                break;
+            }
+        }
     }
 
     private static String buildActionString(DataKind kind, ContentValues values, Context context) {