AI 143424: - Add groups item to edit view and ability to select groups for the contact.
  - Cleaned up groups code in contact view.
  - Fix a couple of small IM bugs in contact view
  BUG=1241747

Automated import of CL 143424
diff --git a/res/layout-finger/edit_contact_entry_group.xml b/res/layout-finger/edit_contact_entry_group.xml
new file mode 100644
index 0000000..b233ca8
--- /dev/null
+++ b/res/layout-finger/edit_contact_entry_group.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/entry_group"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:paddingRight="?android:attr/scrollbarSize"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:background="@android:drawable/list_selector_background"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:focusable="true"
+    android:clickable="true"
+    >
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="14dip"
+        android:layout_marginTop="6dip"
+        android:layout_marginBottom="6dip"
+        android:layout_weight="1"
+        android:duplicateParentState="true"
+        >
+
+        <TextView android:id="@+id/label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal"
+            android:duplicateParentState="true"
+            />
+
+        <TextView android:id="@+id/data"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/label"
+            android:layout_alignLeft="@+id/label"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:duplicateParentState="true"
+            />
+
+    </RelativeLayout>
+
+    <ImageView
+        style="@style/MoreButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        />
+
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0f604fc..7906f4a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -164,6 +164,9 @@
     <!-- Hint text for the postal address field when editing -->
     <string name="ghostData_postal">Postal address</string>
 
+    <!-- Hint text for the group field when editing -->
+    <string name="ghostData_group">Display group</string>
+
     <!-- Message displayed in a toast when you try to view the details of a contact that
          for some reason doesn't exist anymore. -->
     <string name="invalidContactMessage">The contact does not exist.</string>
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 869755b..b8d9fe8 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -108,9 +108,6 @@
     public static final int ORGANIZATIONS_TITLE_COLUMN = 4;
     public static final int ORGANIZATIONS_ISPRIMARY_COLUMN = 5;
     
-    /** Directory for group memberships. */
-    public static final String GROUP_CONTENT_DIRECTORY = "groupmembership";
-    
     protected ArrayList<ArrayList<E>> mSections;
     protected LayoutInflater mInflater;
     protected Context mContext;
diff --git a/src/com/android/contacts/EditContactActivity.java b/src/com/android/contacts/EditContactActivity.java
index b89573b..4d4a423 100644
--- a/src/com/android/contacts/EditContactActivity.java
+++ b/src/com/android/contacts/EditContactActivity.java
@@ -44,6 +44,8 @@
 import static com.android.contacts.ContactEntryAdapter.PHONES_PROJECTION;
 import static com.android.contacts.ContactEntryAdapter.PHONES_TYPE_COLUMN;
 
+import com.android.contacts.ViewContactActivity.ViewEntry;
+
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -69,6 +71,7 @@
 import android.provider.Contacts;
 import android.provider.Contacts.ContactMethods;
 import android.provider.Contacts.Intents.Insert;
+import android.provider.Contacts.GroupMembership;
 import android.provider.Contacts.Groups;
 import android.provider.Contacts.Organizations;
 import android.provider.Contacts.People;
@@ -171,6 +174,20 @@
 
     /** Flag marking this contact as changed, meaning we should write changes back. */
     private boolean mContactChanged = false;
+    
+    /** List of all the group names */
+    private CharSequence[] mGroups;
+    
+    /** Is this contact part of the group */
+    private boolean[] mInTheGroup;
+
+    private static final String[] GROUP_ID_PROJECTION = new String[] {
+        Groups._ID,
+    };
+
+    private static final String[] GROUPMEMBERSHIP_ID_PROJECTION = new String[] {
+        GroupMembership._ID,
+    };
 
     // These are accessed by inner classes. They're package scoped to make access more efficient.
     /* package */ ContentResolver mResolver;
@@ -188,7 +205,7 @@
     /* package */ static final int MSG_ADD_PHONE = 3;
     /* package */ static final int MSG_ADD_EMAIL = 4;
     /* package */ static final int MSG_ADD_POSTAL = 5;
-
+    
     private static final int[] TYPE_PRECEDENCE_PHONES = new int[] {
             Phones.TYPE_MOBILE, Phones.TYPE_HOME, Phones.TYPE_WORK, Phones.TYPE_OTHER
     };
@@ -222,6 +239,12 @@
                 break;
             }
             
+            case R.id.entry_group: {
+                EditEntry entry = findEntryForView(v);
+                doPickGroup(entry);
+                break;
+            }
+            
             case R.id.entry_ringtone: {
                 EditEntry entry = findEntryForView(v);
                 doPickRingtone(entry);
@@ -716,6 +739,121 @@
         setPhotoPresent(false);
     }
     
+    private void populateGroups() {
+        // Create a list of all the groups
+        Cursor cursor = mResolver.query(Groups.CONTENT_URI, ContactsListActivity.GROUPS_PROJECTION,
+                null, null, Groups.DEFAULT_SORT_ORDER);
+        try {
+            ArrayList<Long> ids = new ArrayList<Long>();
+            ArrayList<String> items = new ArrayList<String>();
+
+            while (cursor.moveToNext()) {
+                String systemId = cursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
+                String name = cursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
+                
+                if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
+                    continue;
+                }
+
+                if (!TextUtils.isEmpty(name)) {
+                    ids.add(new Long(cursor.getLong(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID)));
+                    items.add(name);
+                }
+            }
+
+            mGroups = items.toArray(new CharSequence[items.size()]);
+            mInTheGroup = new boolean[items.size()];
+        } finally {
+            cursor.close();
+        }
+        
+        if (mGroups != null) {
+            
+            // Go through the mGroups for this member and update the list
+            final Uri groupsUri = Uri.withAppendedPath(mUri, GroupMembership.CONTENT_DIRECTORY);
+            Cursor groupCursor = mResolver.query(groupsUri, ContactsListActivity.GROUPS_PROJECTION,
+                    null, null, Groups.DEFAULT_SORT_ORDER);
+            if (groupCursor != null) {
+                try {
+                    while (groupCursor.moveToNext()) {
+                        String systemId = groupCursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
+                        String name = groupCursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
+                        
+                        if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
+                            continue;
+                        }
+                        
+                        if (!TextUtils.isEmpty(name)) {
+                            for (int i = 0; i < mGroups.length; i++) {
+                                if (name.equals(mGroups[i])) {
+                                    mInTheGroup[i] = true;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                } finally {
+                    groupCursor.close();
+                }
+            }
+        }
+    }
+    
+    private String generateGroupList() {
+        StringBuilder groupList = new StringBuilder();
+        for (int i = 0; mGroups != null && i < mGroups.length; i++) {
+            if (mInTheGroup[i]) {
+                if (groupList.length() == 0) {
+                    groupList.append(mGroups[i]);
+                } else {
+                    groupList.append(getString(R.string.group_list, mGroups[i]));
+                }
+            }
+        }
+        return groupList.length() > 0 ? groupList.toString() : null;
+    }
+    
+    private void doPickGroup(EditEntry entry) {
+        if (mGroups != null) {
+            GroupDialogListener listener = new GroupDialogListener(this, entry);
+            
+            new AlertDialog.Builder(EditContactActivity.this)
+                .setTitle(R.string.label_groups)
+                .setMultiChoiceItems(mGroups, mInTheGroup, listener)
+                .setPositiveButton(android.R.string.ok, listener)
+                .setNegativeButton(android.R.string.cancel, null)
+                .show();
+        }
+    }
+
+    /** Handles the clicks in the groups dialog */
+    private static final class GroupDialogListener implements DialogInterface.OnClickListener,
+            DialogInterface.OnMultiChoiceClickListener {
+        
+        private EditContactActivity mEditContactActivity;
+        private EditEntry mEntry;
+        private boolean[] mInTheGroup;
+        
+        public GroupDialogListener(EditContactActivity editContactActivity, EditEntry entry) {
+            mEditContactActivity = editContactActivity;
+            mEntry = entry;
+            mInTheGroup = editContactActivity.mInTheGroup.clone();
+        }
+
+        /** Called when the dialog's ok button is clicked */
+        public void onClick(DialogInterface dialog, int which) {
+            mEditContactActivity.mInTheGroup = mInTheGroup;
+            mEntry.data = mEditContactActivity.generateGroupList();
+            mEditContactActivity.updateDataView(mEntry, mEntry.data);
+        }
+
+        /** Called when each group is clicked */
+        public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+            mInTheGroup[which] = isChecked;
+        }
+    }
+    
+    
     private void doPickRingtone(EditEntry entry) {
         Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
         // Allow user to pick 'Default'
@@ -881,6 +1019,64 @@
         }
         finish();
     }
+
+    /**
+     * Gets the group id based on group name.
+     * 
+     * @param resolver the resolver to use
+     * @param groupName the name of the group to add the contact to
+     * @return the id of the group
+     * @throws IllegalStateException if the group can't be found
+     */
+    private long getGroupId(ContentResolver resolver, String groupName) {
+        long groupId = 0;
+        Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUP_ID_PROJECTION,
+                Groups.NAME + "=?", new String[] { groupName }, null);
+        if (groupsCursor != null) {
+            try {
+                if (groupsCursor.moveToFirst()) {
+                    groupId = groupsCursor.getLong(0);
+                }
+            } finally {
+                groupsCursor.close();
+            }
+        }
+    
+        if (groupId == 0) {
+            throw new IllegalStateException("Failed to find the " + groupName + "group");
+        }
+        
+        return groupId;
+    }
+
+    /**
+     * Deletes group membership based on person and group ids.
+     * 
+     * @param personId the person id
+     * @param groupId the group id
+     * @return the id of the group membership
+     */
+    private void deleteGroupMembership(long personId, long groupId) {
+        long groupMembershipId = 0;
+        Cursor groupsCursor = mResolver.query(GroupMembership.CONTENT_URI, GROUPMEMBERSHIP_ID_PROJECTION,
+                GroupMembership.PERSON_ID + "=? AND " + GroupMembership.GROUP_ID + "=?",
+                new String[] {String.valueOf(personId), String.valueOf(groupId)}, null);
+        if (groupsCursor != null) {
+            try {
+                if (groupsCursor.moveToFirst()) {
+                    groupMembershipId = groupsCursor.getLong(0);
+                }
+            } finally {
+                groupsCursor.close();
+            }
+        }
+        
+        if (groupMembershipId != 0) {
+            final Uri groupsUri = ContentUris.withAppendedId(
+                    GroupMembership.CONTENT_URI,groupMembershipId);
+            mResolver.delete(groupsUri, null, null);
+        }
+    }
     
     /**
      * Save the various fields to the existing contact.
@@ -929,6 +1125,18 @@
                     values.put(entry.column, (String) null);
                     mResolver.update(entry.uri, values, null, null);
                 }
+            } else if (kind == EditEntry.KIND_GROUP) {
+                if (entry.id != 0) {
+                    for (int g = 0; g < mGroups.length; g++) {
+                        long groupId = getGroupId(mResolver, mGroups[g].toString());
+                        if (mInTheGroup[g]) {
+                            Contacts.People.addToGroup(mResolver, entry.id, groupId);
+                            numValues++;
+                        } else {
+                            deleteGroupMembership(entry.id, groupId);
+                        }
+                    }
+                }
             } else {
                 if (!empty) {
                     values.clear();
@@ -1152,7 +1360,15 @@
                     mUri);
             mNoteEntries.add(entry);
         }
-
+        
+        // Groups
+        populateGroups();
+        if (mGroups != null) {
+            entry = EditEntry.newGroupEntry(this, generateGroupList(), mUri,
+                    personCursor.getLong(0));
+            mOtherEntries.add(entry);
+        }
+        
         // Ringtone
         entry = EditEntry.newRingtoneEntry(this,
                 personCursor.getString(CONTACT_CUSTOM_RINGTONE_COLUMN), mUri);
@@ -1316,6 +1532,13 @@
             mEmailEntries.add(entry);
         }
 
+        // Group
+        populateGroups();
+        if (mGroups != null) {
+            entry = EditEntry.newGroupEntry(this, null, mUri, 0);
+            mOtherEntries.add(entry);
+        }
+        
         // Ringtone
         entry = EditEntry.newRingtoneEntry(this, null, mUri);
         mOtherEntries.add(entry);
@@ -1577,6 +1800,9 @@
         // with some additional logic.
         if (entry.kind == Contacts.KIND_ORGANIZATION) {
             view = mInflater.inflate(R.layout.edit_contact_entry_org, parent, false);
+        } else if (isOtherEntry(entry, GroupMembership.GROUP_ID)) {
+            view = mInflater.inflate(R.layout.edit_contact_entry_group, parent, false);
+            view.setOnFocusChangeListener(this);
         } else if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
             view = mInflater.inflate(R.layout.edit_contact_entry_ringtone, parent, false);
             view.setOnFocusChangeListener(this);
@@ -1671,6 +1897,10 @@
     private void fillViewData(final EditEntry entry) {
         if (isOtherEntry(entry, People.CUSTOM_RINGTONE)) {
             updateRingtoneView(entry);
+        } else if (isOtherEntry(entry, GroupMembership.GROUP_ID)) {
+            if (entry.data != null) {
+                updateDataView(entry, entry.data);
+            }
         } else if (isOtherEntry(entry, People.SEND_TO_VOICEMAIL)) {
             CheckBox checkBox = (CheckBox) entry.view.findViewById(R.id.checkbox);
             boolean sendToVoicemail = false;
@@ -1846,7 +2076,9 @@
                 }
 
                 case Contacts.KIND_IM: {
-                    v.setText(getLabelsForKind(activity, kind)[type]);
+                    if (type >= 0) {
+                        v.setText(getLabelsForKind(activity, kind)[type]);
+                    }
                     break;
                 }
                 
@@ -2036,6 +2268,24 @@
         }
 
         /**
+         * Create a new group entry with the given data.
+         */
+        public static final EditEntry newGroupEntry(EditContactActivity activity,
+                String data, Uri uri, long personId) {
+            EditEntry entry = new EditEntry(activity);
+            entry.label = activity.getString(R.string.label_groups);
+            entry.data = data;
+            entry.uri = uri;
+            entry.id = personId;
+            entry.column = GroupMembership.GROUP_ID;
+            entry.kind = KIND_GROUP;
+            entry.isStaticLabel = true;
+            entry.syncDataWithView = false;
+            entry.lines = -1;
+            return entry;
+        }
+
+        /**
          * Create a new ringtone entry with the given data.
          */
         public static final EditEntry newRingtoneEntry(EditContactActivity activity,
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index d3c51f3..e5afdc2 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -74,6 +74,7 @@
 import android.provider.Im;
 import android.provider.Contacts.ContactMethods;
 import android.provider.Contacts.Groups;
+import android.provider.Contacts.GroupMembership;
 import android.provider.Contacts.Organizations;
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
@@ -729,7 +730,7 @@
                     case Contacts.KIND_IM: {
                         Object protocolObj = ContactMethods.decodeImProtocol(
                                 methodsCursor.getString(METHODS_AUX_DATA_COLUMN));
-                        String host;
+                        String host = null;
                         if (protocolObj instanceof Number) {
                             int protocol = ((Number) protocolObj).intValue();
                             entry.label = buildActionString(R.string.actionChat,
@@ -739,7 +740,7 @@
                                     || protocol == ContactMethods.PROTOCOL_MSN) {
                                 entry.maxLabelLines = 2;
                             }
-                        } else {
+                        } else if (protocolObj != null) {
                             String providerName = (String) protocolObj;
                             entry.label = buildActionString(R.string.actionChat,
                                     providerName, false);
@@ -861,49 +862,6 @@
             organizationsCursor.close();
         }
 
-        // Build the group entries
-        final Uri groupsUri = Uri.withAppendedPath(mUri,
-                ContactEntryAdapter.GROUP_CONTENT_DIRECTORY);
-        Cursor cursor = mResolver.query(groupsUri, ContactsListActivity.GROUPS_PROJECTION,
-                null, null, Groups.DEFAULT_SORT_ORDER);
-        try {
-            ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
-            ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();
-            StringBuilder sb = new StringBuilder();
-            
-            while (cursor.moveToNext()) {
-                String systemId = cursor.getString(
-                        ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
-                
-                if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
-                    continue;
-                }
-                
-                String name = cursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
-                if (!TextUtils.isEmpty(name)) {
-                    if (sb.length() == 0) {
-                        sb.append(name);
-                    } else {
-                        sb.append(getString(R.string.group_list, name));
-                    }
-                }
-            }
-            
-            if (sb.length() > 0) {               
-                ViewEntry entry = new ViewEntry();
-                entry.kind = ContactEntryAdapter.Entry.KIND_GROUP;
-                entry.label = getString(R.string.label_groups);
-                entry.data = sb.toString();
-                entry.intent = new Intent(Intent.ACTION_EDIT, mUri);
-                
-                // TODO: Add an icon for the groups item.
-                
-                mGroupEntries.add(entry);
-            }
-        } finally {
-            cursor.close();
-        }
-
         // Build the other entries
         String note = personCursor.getString(CONTACT_NOTES_COLUMN);
         if (!TextUtils.isEmpty(note)) {
@@ -918,7 +876,49 @@
             entry.actionIcon = R.drawable.sym_note;
             mOtherEntries.add(entry);
         }
-        
+
+        // Build the group entries
+        final Uri groupsUri = Uri.withAppendedPath(mUri, GroupMembership.CONTENT_DIRECTORY);
+        Cursor groupCursor = mResolver.query(groupsUri, ContactsListActivity.GROUPS_PROJECTION,
+                null, null, Groups.DEFAULT_SORT_ORDER);
+        if (groupCursor != null) {
+            try {
+                StringBuilder sb = new StringBuilder();
+
+                while (groupCursor.moveToNext()) {
+                    String systemId = groupCursor.getString(
+                            ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
+
+                    if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
+                        continue;
+                    }
+
+                    String name = groupCursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
+                    if (!TextUtils.isEmpty(name)) {
+                        if (sb.length() == 0) {
+                            sb.append(name);
+                        } else {
+                            sb.append(getString(R.string.group_list, name));
+                        }
+                    }
+                }
+
+                if (sb.length() > 0) {
+                    ViewEntry entry = new ViewEntry();
+                    entry.kind = ContactEntryAdapter.Entry.KIND_GROUP;
+                    entry.label = getString(R.string.label_groups);
+                    entry.data = sb.toString();
+                    entry.intent = new Intent(Intent.ACTION_EDIT, mUri);
+
+                    // TODO: Add an icon for the groups item.
+
+                    mGroupEntries.add(entry);
+                }
+            } finally {
+                groupCursor.close();
+            }
+        }
+
         // Build the ringtone entry
         String ringtoneStr = personCursor.getString(CONTACT_CUSTOM_RINGTONE_COLUMN);
         if (!TextUtils.isEmpty(ringtoneStr)) {