am d5a1530f: Make the dialpad silent in "vibrate only" mode (bug 1984905).

Merge commit 'd5a1530f68c48075cd3df5c948b54ad34f059df1'

* commit 'd5a1530f68c48075cd3df5c948b54ad34f059df1':
  Make the dialpad silent in "vibrate only" mode (bug 1984905).
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9ff4b4b..4cde2b7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,9 +15,10 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.contacts"
-        android:sharedUserId="android.uid.shared"
+    package="com.android.contacts"
+    android:sharedUserId="android.uid.shared"
 >
+
     <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
@@ -108,7 +109,7 @@
                 <category android:name="android.intent.category.BROWSABLE" />
             </intent-filter>
         </activity>
-        
+
         <!-- An empty activity that presents the DialtactActivity's Contacts tab -->
         <activity-alias android:name="DialtactsContactsEntryActivity"
             android:targetActivity="DialtactsActivity"
@@ -126,7 +127,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.dir/person" />
             </intent-filter>
-            
+
             <intent-filter>
                 <action android:name="com.android.contacts.action.FILTER_CONTACTS" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -144,7 +145,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity-alias>
-        
+
         <!-- The actual list of contacts, usually embedded in ContactsActivity -->
         <activity android:name="ContactsListActivity"
             android:label="@string/contactsList"
@@ -179,7 +180,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.TAB" />
             </intent-filter>
-            
+
             <intent-filter android:label="@string/frequentList">
                 <action android:name="com.android.contacts.action.LIST_FREQUENT" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -197,14 +198,7 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.item/person" />
             </intent-filter>
-<!--
-            <intent-filter android:label="Add To Contacts">
-                <action android:name="com.android.contacts.action.ADD_CONTACT" />
-                <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
-                <data android:scheme="mailto" />
-                <data android:scheme="tel" />
-            </intent-filter>
--->
+
             <intent-filter>
                 <action android:name="android.intent.action.PICK" />
                 <category android:name="android.intent.category.DEFAULT" />
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/config.xml b/res/values/config.xml
index d7a3be4..7af1d54 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -19,7 +19,7 @@
 
 <resources>
     <!-- Flag indicating whether Contacts app is allowed to import contacts from SDCard -->
-    <bool name="config_allow_import_from_sdcard">false</bool>
+    <bool name="config_allow_import_from_sdcard">true</bool>
     <!-- If true, all vcard files are imported from SDCard without asking a user.
     If not, dialog shows to let the user to select whether all vcard files are imported or not.
     If the user selects "not", then the application ask the user to select a file.-->
@@ -34,6 +34,15 @@
     <!-- Flag indicating whether Contacts app is allowed to export contacts to SDCard -->
     <bool name="config_allow_export_to_sdcard">false</bool>
 
+    <!-- If true, enable vibration (haptic feedback) for dialer key presses.
+         TODO: If enough users are annoyed by this, we might eventually
+         need to make it a user preference rather than a per-platform
+         resource. -->
+    <bool name="config_enable_dialer_key_vibration">true</bool>
+
+    <!-- How long to vibrate (in msec), if dialer key vibration is enabled. -->
+    <integer name="config_dialer_key_vibrate_duration">40</integer>
+
     <!-- The type of VCard for export. Without specifying this, generic VCard will be emitted.
      If you want to let the app emit VCard specific to some vendor (like DoCoMo),
      please specify the type.-->
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c6add99..3aba0a4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -145,6 +145,15 @@
     <!-- The label describing the custom ringtone for a contact -->
     <string name="label_ringtone">Ringtone</string>
 
+    <!-- The label for a list of all the groups that the contact is associated with -->
+    <string name="label_groups">Groups</string>
+
+    <!-- Provides a delimeter in a list of groups. For example, "g1, g2" has ", g2" generated using this string -->
+    <string name="group_list">, <xliff:g id="groupName">%s</xliff:g></string>
+
+    <!-- Menu item used to send user to the edit contact screen -->
+    <string name="menu_viewGroup">Edit groups</string>
+
     <!-- Hint text for the contact name when editing -->
     <string name="ghostData_name">First and Last</string>
 
@@ -172,6 +181,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>
@@ -257,7 +269,7 @@
     <!-- Title for group selection dialog. The dialog contains a list of contact groups that the
          user can pick from, indicating they only want to see the contacts in that group. -->
     <string name="select_group_title">Groups</string>
-    
+
     <!-- The text displayed when the contacts list is empty while displaying a single group of contacts -->
     <string name="groupEmpty">Your \"<xliff:g id="groupName">%s</xliff:g>\" group is empty.</string>
 
@@ -312,6 +324,9 @@
     <!-- Separator in the contact details list describing that the items below are non-actionable organization information -->
     <string name="listSeparatorOrganizations">Organizations</string>
 
+    <!-- Separator in the contact details list describing that the items below are non-actionable group information -->
+    <string name="listSeparatorGroups">Groups</string>
+
     <!-- Separator in the contact details list describing that the items below are random other non-actionable information about a contact -->
     <string name="listSeparatorOtherInformation">Other information</string>
 
@@ -360,6 +375,9 @@
     <!-- The title of a dialog that displays the IMEI of the phone -->
     <string name="imei">IMEI</string>
 
+    <!-- The title of a dialog that displays the MEID of the CDMA phone -->
+    <string name="meid">MEID</string>
+
     <!-- String used for displaying calls to the voicemail number in the call log -->
     <string name="voicemail">Voicemail</string>
 
@@ -636,7 +654,10 @@
     <string name="fail_reason_io_error">I/O Error</string>
 
     <!-- The failed reason: "Failed to parse VCard data" -->
-    <string name="fail_reason_vcard_parse_error">Failed to parse VCard</string>
+    <string name="fail_reason_vcard_parse_error">Failed to parse VCard with some unexpected reason</string>
+    
+    <!-- The failed reason: "The VCard is not supported right now, but may be supported in the future" -->
+    <string name="fail_reason_vcard_not_supported_error">Failed to parse VCard though it seems in valid format, since the current implementation does not support it</string>
 
     <!-- The failed reason: "There is no VCard file" -->
     <string name="fail_reason_no_vcard_file">No VCard file found on SD Card</string>
@@ -715,4 +736,4 @@
     
     <!-- The string used to describe Contacts as a searchable item within system search settings. -->
     <string name="search_settings_description">Names of your contacts</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index c5b7ccf..b8d9fe8 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -122,7 +122,9 @@
         /** Synthesized phone entry that will send an SMS instead of call the number */
         public static final int KIND_SMS = -2;
         /** A section separator */
-        public static final int KIND_SEPARATOR = -3; 
+        public static final int KIND_SEPARATOR = -3;
+        /** Signifies a group row that is stored in the group membership table */
+        public static final int KIND_GROUP = -4;
 
         public String label;
         public String data;
diff --git a/src/com/android/contacts/ContactsGroupSyncSelector.java b/src/com/android/contacts/ContactsGroupSyncSelector.java
index e1fbdf1..a94f8db 100644
--- a/src/com/android/contacts/ContactsGroupSyncSelector.java
+++ b/src/com/android/contacts/ContactsGroupSyncSelector.java
@@ -16,14 +16,15 @@
 
 package com.android.contacts;
 
-import com.google.android.googlelogin.GoogleLoginServiceConstants;
-import com.google.android.googlelogin.GoogleLoginServiceHelper;
-
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.Future2;
+import android.accounts.Future2Callback;
+import android.accounts.OperationCanceledException;
 import android.app.ListActivity;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Intent;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.provider.Contacts;
@@ -37,6 +38,9 @@
 import android.widget.ListView;
 
 import java.util.ArrayList;
+import java.io.IOException;
+
+import com.google.android.googlelogin.GoogleLoginServiceConstants;
 
 public final class ContactsGroupSyncSelector extends ListActivity implements View.OnClickListener {
 
@@ -51,8 +55,6 @@
     private static final int COLUMN_INDEX_SHOULD_SYNC = 2;
     private static final int COLUMN_INDEX_SYSTEM_ID = 3;
 
-    private static final int SUBACTIVITY_GET_ACCOUNT = 1;
-
     ArrayList<Boolean> mChecked;
     ArrayList<Long> mGroupIds;
     boolean mSyncAllGroups;
@@ -160,9 +162,26 @@
             // through onActivityResult().
             Bundle bundle = new Bundle();
             bundle.putCharSequence("optional_message", getText(R.string.contactsSyncPlug));
-            GoogleLoginServiceHelper.getCredentials(this, SUBACTIVITY_GET_ACCOUNT,
-                    bundle, GoogleLoginServiceConstants.PREFER_HOSTED, Gmail.GMAIL_AUTH_SERVICE,
-                    true);
+            AccountManager.get(this).getAuthTokenByFeatures(
+                    GoogleLoginServiceConstants.ACCOUNT_TYPE, Gmail.GMAIL_AUTH_SERVICE,
+                    new String[]{GoogleLoginServiceConstants.FEATURE_GOOGLE_OR_DASHER}, this,
+                    bundle, null /* loginOptions */, new Future2Callback() {
+                public void run(Future2 future) {
+                    try {
+                        // do this to check if this request succeeded or not
+                        future.getResult();
+                        // There is an account setup, build the group list
+                        buildItems();
+                        adjustChecks();
+                    } catch (OperationCanceledException e) {
+                        finish();
+                    } catch (IOException e) {
+                        finish();
+                    } catch (AuthenticatorException e) {
+                        finish();
+                    }
+                }
+            }, null /* handler */);
         }
 
         setContentView(R.layout.sync_settings);
@@ -173,20 +192,6 @@
         getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
     }
 
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
-        super.onActivityResult(requestCode, resultCode, intent);
-        if (requestCode == SUBACTIVITY_GET_ACCOUNT) {
-            if (resultCode == RESULT_OK) {
-                // There is an account setup, build the group list
-                buildItems();
-                adjustChecks();
-            } else {
-                finish();
-            }
-        }
-    }
-
     private void buildItems() {
         final ContentResolver resolver = getContentResolver();
         Cursor cursor = resolver.query(Groups.CONTENT_URI, PROJECTION, null, null, Groups.NAME);
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 02c70d2..cb06b0d 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -258,12 +258,12 @@
 
     private static final int QUERY_TOKEN = 42;
 
-    private static final String[] GROUPS_PROJECTION = new String[] {
+    static final String[] GROUPS_PROJECTION = new String[] {
         Groups.SYSTEM_ID, // 0
         Groups.NAME, // 1
     };
-    private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
-    private static final int GROUPS_COLUMN_INDEX_NAME = 1;
+    static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
+    static final int GROUPS_COLUMN_INDEX_NAME = 1;
     
     static final String GROUP_WITH_PHONES = "android_smartgroup_phone";
 
@@ -522,23 +522,25 @@
         mQueryHandler = new QueryHandler(this);
         mJustCreated = true;
 
-        // Check to see if sync is enabled
-        final ContentResolver resolver = getContentResolver();
-        IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
-        if (provider == null) {
-            // No contacts provider, bail.
-            finish();
-            return;
-        }
-
-        try {
-            ISyncAdapter sa = provider.getSyncAdapter();
-            mSyncEnabled = sa != null;
-        } catch (RemoteException e) {
-            mSyncEnabled = false;
-        } finally {
-            resolver.releaseProvider(provider);
-        }
+        // TODO(jham) redesign this
+        mSyncEnabled = true;
+//        // Check to see if sync is enabled
+//        final ContentResolver resolver = getContentResolver();
+//        IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
+//        if (provider == null) {
+//            // No contacts provider, bail.
+//            finish();
+//            return;
+//        }
+//
+//        try {
+//            ISyncAdapter sa = provider.getSyncAdapter();
+//            mSyncEnabled = sa != null;
+//        } catch (RemoteException e) {
+//            mSyncEnabled = false;
+//        } finally {
+//            resolver.releaseProvider(provider);
+//        }
     }
 
     private void setEmptyText() {
diff --git a/src/com/android/contacts/DialtactsActivity.java b/src/com/android/contacts/DialtactsActivity.java
index 73d702b..8f933b8 100644
--- a/src/com/android/contacts/DialtactsActivity.java
+++ b/src/com/android/contacts/DialtactsActivity.java
@@ -213,6 +213,8 @@
             }
         } else if (FAVORITES_ENTRY_COMPONENT.equals(componentName)) {
             mTabHost.setCurrentTab(TAB_INDEX_FAVORITES);
+        } else if (Contacts.Intents.UI.FILTER_CONTACTS_ACTION.equals(intent.getAction())) {
+            mTabHost.setCurrentTab(TAB_INDEX_CONTACTS);
         } else {
             SharedPreferences prefs = getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE);
             boolean favoritesAsContacts = prefs.getBoolean(PREF_FAVORITES_AS_CONTACTS,
diff --git a/src/com/android/contacts/EditContactActivity.java b/src/com/android/contacts/EditContactActivity.java
index b89573b..4d5a669 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,127 @@
         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 groups for this member and update the list
+            final Uri groupsUri = Uri.withAppendedPath(mUri, GroupMembership.CONTENT_DIRECTORY);
+            Cursor groupCursor = null;
+            try {
+                groupCursor = mResolver.query(groupsUri, ContactsListActivity.GROUPS_PROJECTION,
+                        null, null, Groups.DEFAULT_SORT_ORDER);
+            } catch (IllegalArgumentException e) {
+                // Contact is new, so we don't need to do any work.
+            }
+            
+            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 +1025,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 +1131,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();
@@ -1045,7 +1259,16 @@
         int entryCount = ContactEntryAdapter.countEntries(mSections, false);
         for (int i = 0; i < entryCount; i++) {
             EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
-            if (entry.kind != EditEntry.KIND_CONTACT) {
+            if (entry.kind == EditEntry.KIND_GROUP) {
+                long contactId = ContentUris.parseId(contactUri);
+                for (int g = 0; g < mGroups.length; g++) {
+                    if (mInTheGroup[g]) {
+                        long groupId = getGroupId(mResolver, mGroups[g].toString());
+                        People.addToGroup(mResolver, contactId, groupId);
+                        numValues++;
+                    }
+                }
+            } else if (entry.kind != EditEntry.KIND_CONTACT) {
                 values.clear();
                 if (entry.toValues(values)) {
                     // Only create the entry if there is data
@@ -1152,7 +1375,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 +1547,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 +1815,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 +1912,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 +2091,9 @@
                 }
 
                 case Contacts.KIND_IM: {
-                    v.setText(getLabelsForKind(activity, kind)[type]);
+                    if (type >= 0) {
+                        v.setText(getLabelsForKind(activity, kind)[type]);
+                    }
                     break;
                 }
                 
@@ -2036,6 +2283,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/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 07eb821..b1dd11a 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -27,17 +27,19 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.syncml.pim.VBuilder;
-import android.syncml.pim.VBuilderCollection;
-import android.syncml.pim.VParser;
-import android.syncml.pim.vcard.VCardDataBuilder;
-import android.syncml.pim.vcard.VCardEntryCounter;
-import android.syncml.pim.vcard.VCardException;
-import android.syncml.pim.vcard.VCardNestedException;
-import android.syncml.pim.vcard.VCardParser_V21;
-import android.syncml.pim.vcard.VCardParser_V30;
-import android.syncml.pim.vcard.VCardSourceDetector;
-import android.syncml.pim.vcard.VCardVersionException;
+import android.pim.vcard.EntryCommitter;
+import android.pim.vcard.VCardBuilder;
+import android.pim.vcard.VCardBuilderCollection;
+import android.pim.vcard.VCardConfig;
+import android.pim.vcard.VCardDataBuilder;
+import android.pim.vcard.VCardEntryCounter;
+import android.pim.vcard.VCardParser_V21;
+import android.pim.vcard.VCardParser_V30;
+import android.pim.vcard.VCardSourceDetector;
+import android.pim.vcard.exception.VCardException;
+import android.pim.vcard.exception.VCardNestedException;
+import android.pim.vcard.exception.VCardNotSupportedException;
+import android.pim.vcard.exception.VCardVersionException;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.RelativeSizeSpan;
@@ -182,18 +184,19 @@
                     }
                     VCardEntryCounter counter = new VCardEntryCounter();
                     VCardSourceDetector detector = new VCardSourceDetector();
-                    VBuilderCollection builderCollection = new VBuilderCollection(
+                    VCardBuilderCollection builderCollection = new VCardBuilderCollection(
                             Arrays.asList(counter, detector));
+
                     boolean result;
                     try {
-                        result = readOneVCard(mCanonicalPath,
-                                VParser.DEFAULT_CHARSET, builderCollection, null, true);
+                        result = readOneVCardFile(mCanonicalPath,
+                                VCardConfig.DEFAULT_CHARSET, builderCollection, null, true);
                     } catch (VCardNestedException e) {
                         try {
                             // Assume that VCardSourceDetector was able to detect the source.
                             // Try again with the detector.
-                            result = readOneVCard(mCanonicalPath,
-                                    VParser.DEFAULT_CHARSET, counter, detector, false);
+                            result = readOneVCardFile(mCanonicalPath,
+                                    VCardConfig.DEFAULT_CHARSET, counter, detector, false);
                         } catch (VCardNestedException e2) {
                             result = false;
                             Log.e(LOG_TAG, "Must not reach here. " + e2);
@@ -227,8 +230,8 @@
 
                         VCardSourceDetector detector = new VCardSourceDetector();
                         try {
-                            if (!readOneVCard(canonicalPath, VParser.DEFAULT_CHARSET, detector,
-                                    null, true)) {
+                            if (!readOneVCardFile(canonicalPath, VCardConfig.DEFAULT_CHARSET,
+                                    detector, null, true)) {
                                 continue;
                             }
                         } catch (VCardNestedException e) {
@@ -248,45 +251,40 @@
 
         private void doActuallyReadOneVCard(String charset, boolean doIncrementProgress,
                 VCardSourceDetector detector) {
-            VCardDataBuilder builder;
             final Context context = ImportVCardActivity.this;
+            VCardDataBuilder builder;
+            int nameOrderType =
+                (mLastNameComesBeforeFirstName ? 
+                        VCardConfig.NAME_ORDER_TYPE_JAPANESE :
+                            VCardConfig.NAME_ORDER_TYPE_ENGLISH);
             if (charset != null) {
-                builder = new VCardDataBuilder(mResolver,
-                        mProgressDialog,
-                        context.getString(R.string.reading_vcard_message),
-                        mHandler,
-                        charset,
+                builder = new VCardDataBuilder(charset,
                         charset,
                         false,
-                        mLastNameComesBeforeFirstName);
+                        nameOrderType);
             } else {
-                builder = new VCardDataBuilder(mResolver,
-                        mProgressDialog,
-                        context.getString(R.string.reading_vcard_message),
-                        mHandler,
-                        null,
+                charset = VCardConfig.DEFAULT_CHARSET;
+                builder = new VCardDataBuilder(null,
                         null,
                         false,
-                        mLastNameComesBeforeFirstName);
-                charset = VParser.DEFAULT_CHARSET;
+                        nameOrderType);
             }
-            if (doIncrementProgress) {
-                builder.setOnProgressRunnable(new Runnable() {
-                    public void run() {
-                        mProgressDialog.incrementProgressBy(1);
-                    }
-                });
-            }
+            builder.addEntryHandler(new EntryCommitter(mResolver));
+            builder.addEntryHandler(new ProgressShower(mProgressDialog,
+                    context.getString(R.string.reading_vcard_message),
+                    mHandler,
+                    doIncrementProgress));
+
             try {
-                readOneVCard(mCanonicalPath, charset, builder, detector, false);
+                readOneVCardFile(mCanonicalPath, charset, builder, detector, false);
             } catch (VCardNestedException e) {
-                Log.e(LOG_TAG, "Must not reach here.");
+                Log.e(LOG_TAG, "Never reach here.");
             }
-            builder.showDebugInfo();
         }
 
-        private boolean readOneVCard(String canonicalPath, String charset, VBuilder builder,
-                VCardSourceDetector detector, boolean throwNestedException)
+        private boolean readOneVCardFile(String canonicalPath, String charset,
+                VCardBuilder builder, VCardSourceDetector detector,
+                boolean throwNestedException)
                 throws VCardNestedException {
             FileInputStream is;
             try {
@@ -316,9 +314,8 @@
                         }
                     }
                 }
-                mVCardParser.showDebugInfo();
             } catch (IOException e) {
-                Log.e(LOG_TAG, "IOException was emitted: " + e);
+                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
 
                 mProgressDialog.dismiss();
 
@@ -326,19 +323,15 @@
                         getString(R.string.fail_reason_io_error) +
                         " (" + e.getMessage() + ")"));
                 return false;
-            } catch (VCardNestedException e) {
-                if (throwNestedException) {
-                    throw e;
-                } else {
-                    Log.e(LOG_TAG, "VCardNestedException was emitted: " + e);
-                    mHandler.post(new ErrorDisplayer(
-                            getString(R.string.fail_reason_vcard_parse_error) +
-                            " (" + e.getMessage() + ")"));
-                    return false;
+            } catch (VCardNotSupportedException e) {
+                if ((e instanceof VCardNestedException) && throwNestedException) {
+                    throw (VCardNestedException)e;
                 }
+                mHandler.post(new ErrorDisplayer(
+                        getString(R.string.fail_reason_vcard_not_supported_error) +
+                        " (" + e.getMessage() + ")"));
+                return false;
             } catch (VCardException e) {
-                Log.e(LOG_TAG, "VCardException was emitted: " + e);
-
                 mHandler.post(new ErrorDisplayer(
                         getString(R.string.fail_reason_vcard_parse_error) +
                         " (" + e.getMessage() + ")"));
diff --git a/src/com/android/contacts/ProgressShower.java b/src/com/android/contacts/ProgressShower.java
new file mode 100644
index 0000000..9498569
--- /dev/null
+++ b/src/com/android/contacts/ProgressShower.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2009 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;
+
+import android.app.ProgressDialog;
+import android.os.Handler;
+import android.pim.vcard.ContactStruct;
+import android.pim.vcard.EntryHandler;
+import android.pim.vcard.VCardConfig;
+import android.util.Log;
+
+public class ProgressShower implements EntryHandler {
+    public static final String LOG_TAG = "vcard.ProgressShower"; 
+
+    private final Handler mHandler;
+    private final ProgressDialog mProgressDialog;
+    private final String mProgressMessage;
+    private final boolean mIncrementProgress;
+
+    private long mTime;
+    
+    private class ShowProgressRunnable implements Runnable {
+        private ContactStruct mContact;
+        
+        public ShowProgressRunnable(ContactStruct contact) {
+            mContact = contact;
+        }
+        
+        public void run() {
+            mProgressDialog.setMessage(mProgressMessage + "\n" + 
+                    mContact.displayString());
+            if (mIncrementProgress) {
+                mProgressDialog.incrementProgressBy(1);
+            }
+        }
+    }
+    
+    public ProgressShower(ProgressDialog progressDialog,
+            String progressMessage,
+            Handler handler, 
+            boolean incrementProgress) {
+        mHandler = handler;
+        mProgressDialog = progressDialog;
+        mProgressMessage = progressMessage;
+        mIncrementProgress = incrementProgress;
+    }
+    
+    public void onEntryCreated(ContactStruct contactStruct) {
+        long start = System.currentTimeMillis();
+        
+        if (!contactStruct.isIgnorable()) {
+            if (mProgressDialog != null && mProgressMessage != null) {
+                if (mHandler != null) {
+                    mHandler.post(new ShowProgressRunnable(contactStruct));
+                } else {
+                    mProgressDialog.setMessage(mProgressMessage + "\n" + 
+                            contactStruct.displayString());
+                }
+            }
+        }
+        
+        mTime += System.currentTimeMillis() - start;
+    }
+
+    public void onFinal() {
+        if (VCardConfig.showPerformanceLog()) {
+            Log.d(LOG_TAG,
+                    String.format("Time to progress a dialog: %ld ms", mTime));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index 0732ffe..75af4ae 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -41,6 +41,7 @@
  * connected with a specific E-mail address or phone number. Will search based
  * on incoming {@link Intent#getData()} as described by
  * {@link Intents#SHOW_OR_CREATE_CONTACT}.
+ *
  * <ul>
  * <li>If no matching contacts found, will prompt user with dialog to add to a
  * contact, then will use {@link Intent#ACTION_INSERT_OR_EDIT} to let create new
diff --git a/src/com/android/contacts/SpecialCharSequenceMgr.java b/src/com/android/contacts/SpecialCharSequenceMgr.java
index 999e141..3f2c304 100644
--- a/src/com/android/contacts/SpecialCharSequenceMgr.java
+++ b/src/com/android/contacts/SpecialCharSequenceMgr.java
@@ -181,12 +181,19 @@
         if (input.equals(MMI_IMEI_DISPLAY)) {
             int networkType = ((TelephonyManager)context.getSystemService(
                     Context.TELEPHONY_SERVICE)).getNetworkType();
-            // check for GSM
-            if(networkType == TelephonyManager.NETWORK_TYPE_GPRS || 
-                    networkType == TelephonyManager.NETWORK_TYPE_EDGE || 
-                    networkType == TelephonyManager.NETWORK_TYPE_UMTS ) { 
 
-                showIMEIPanel(context, useSystemWindow); 
+            if (networkType == TelephonyManager.NETWORK_TYPE_GPRS ||
+                    networkType == TelephonyManager.NETWORK_TYPE_EDGE ||
+                    networkType == TelephonyManager.NETWORK_TYPE_UMTS) {
+
+                showIMEIPanel(context, useSystemWindow);
+                return true;
+            } else if (networkType == TelephonyManager.NETWORK_TYPE_CDMA ||
+                         networkType == TelephonyManager.NETWORK_TYPE_EVDO_0 ||
+                         networkType == TelephonyManager.NETWORK_TYPE_EVDO_A ||
+                         networkType == TelephonyManager.NETWORK_TYPE_1xRTT) {
+
+                showMEIDPanel(context, useSystemWindow);
                 return true;
             }
         }
@@ -207,6 +214,19 @@
         alert.getWindow().setType(WindowManager.LayoutParams.TYPE_PRIORITY_PHONE);
     }
 
+    static void showMEIDPanel(Context context, boolean useSystemWindow) {
+        String meidStr = ((TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE))
+                .getDeviceId();
+
+        AlertDialog alert = new AlertDialog.Builder(context)
+                .setTitle(R.string.meid)
+                .setMessage(meidStr)
+                .setPositiveButton(android.R.string.ok, null)
+                .setCancelable(false)
+                .show();
+        alert.getWindow().setType(WindowManager.LayoutParams.TYPE_PRIORITY_PHONE);
+    }
+
     /*******
      * This code is used to handle SIM Contact queries
      *******/
diff --git a/src/com/android/contacts/TwelveKeyDialer.java b/src/com/android/contacts/TwelveKeyDialer.java
index 33f261e..2277e56 100644
--- a/src/com/android/contacts/TwelveKeyDialer.java
+++ b/src/com/android/contacts/TwelveKeyDialer.java
@@ -34,6 +34,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.os.Vibrator;
 import android.provider.Contacts.Intents.Insert;
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
@@ -98,7 +99,12 @@
 
     // determines if we want to playback local DTMF tones.
     private boolean mDTMFToneEnabled;
-    
+
+    // Vibration (haptic feedback) for dialer key presses.
+    private Vibrator mVibrator;
+    private boolean mVibrateOn;
+    private long mVibrateDuration;
+
     /** Identifier for the "Add Call" intent extra. */
     static final String ADD_CALL_MODE_KEY = "add_call_mode";
     /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
@@ -215,6 +221,13 @@
                 }
             }
         }
+
+        // Initialize vibration parameters.
+        // TODO: We might eventually need to make mVibrateOn come from a
+        // user preference rather than a per-platform resource, in which
+        // case we would need to update it in onResume() rather than here.
+        mVibrateOn = r.getBoolean(R.bool.config_enable_dialer_key_vibration);
+        mVibrateDuration = (long) r.getInteger(R.integer.config_dialer_key_vibrate_duration);
     }
 
     @Override
@@ -371,7 +384,7 @@
     @Override
     protected void onResume() {
         super.onResume();
-        
+
         // retrieve the DTMF tone play back setting.
         mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
                 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
@@ -534,6 +547,7 @@
     }
     
     private void keyPressed(int keyCode) {
+        vibrate();
         KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
         mDigits.onKeyDown(keyCode, event);
     }
@@ -617,6 +631,7 @@
                 return;
             }
             case R.id.digits: {
+                vibrate();  // Vibrate here too, just like we do for the regular keys
                 placeCall();
                 return;
             }
@@ -941,4 +956,17 @@
         }
         return phoneInUse;
     }
+
+    /**
+     * Triggers haptic feedback (if enabled) for dialer key presses.
+     */
+    private synchronized void vibrate() {
+        if (!mVibrateOn) {
+            return;
+        }
+        if (mVibrator == null) {
+            mVibrator = new Vibrator();
+        }
+        mVibrator.vibrate(mVibrateDuration);
+    }
 }
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index 28a82b4..e5afdc2 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -73,6 +73,8 @@
 import android.provider.Contacts;
 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;
@@ -136,6 +138,7 @@
     /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
     /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
     /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
+    /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
     /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
     /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
 
@@ -229,6 +232,7 @@
         mSections.add(mImEntries);
         mSections.add(mPostalEntries);
         mSections.add(mOrganizationEntries);
+        mSections.add(mGroupEntries);
         mSections.add(mOtherEntries);
 
         //TODO Read this value from a preference
@@ -395,6 +399,11 @@
                 menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
                 break;
             }
+            
+            case ContactEntryAdapter.Entry.KIND_GROUP: {
+                menu.add(0, 0, 0, R.string.menu_viewGroup).setIntent(entry.intent);
+                break;
+            }
         }
     }
 
@@ -579,6 +588,11 @@
 
         separator = new ViewEntry();
         separator.kind = ViewEntry.KIND_SEPARATOR;
+        separator.data = getString(R.string.listSeparatorGroups);
+        mGroupEntries.add(separator);
+
+        separator = new ViewEntry();
+        separator.kind = ViewEntry.KIND_SEPARATOR;
         separator.data = getString(R.string.listSeparatorOtherInformation);
         mOtherEntries.add(separator);
     }
@@ -716,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,
@@ -726,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);
@@ -848,7 +862,6 @@
             organizationsCursor.close();
         }
 
-
         // Build the other entries
         String note = personCursor.getString(CONTACT_NOTES_COLUMN);
         if (!TextUtils.isEmpty(note)) {
@@ -863,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)) {