Implement phone search in DialtactsActivity

Includes tiny layout fixes for ContactSelectionActivity,
which also uses the same phone search fragment.

TODO: let the other Adapters like DefaultContactPickerFragment,
      EmailAddressPickerFragment support grouping feature.

Change-Id: I8d7718192522a0005b9b76931560fe297cad882f
diff --git a/res/layout/contact_picker.xml b/res/layout/contact_picker.xml
index 6b03501..c3fe2fa 100644
--- a/res/layout/contact_picker.xml
+++ b/res/layout/contact_picker.xml
@@ -18,8 +18,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     class="com.android.contacts.widget.FullHeightLinearLayout"
     style="@style/ContactPickerLayout"
-    android:paddingLeft="32dip"
-    android:paddingRight="32dip"
+    android:paddingLeft="8dip"
+    android:paddingRight="8dip"
     android:orientation="vertical"
     android:layout_height="match_parent">
     <view
diff --git a/res/layout/dialtacts_activity.xml b/res/layout/dialtacts_activity.xml
index 7a9e7b2..675dcdb 100644
--- a/res/layout/dialtacts_activity.xml
+++ b/res/layout/dialtacts_activity.xml
@@ -45,4 +45,12 @@
         class="com.android.contacts.list.StrequentContactListFragment"
         android:layout_height="match_parent"
         android:layout_width="match_parent" />
-</FrameLayout>
\ No newline at end of file
+
+    <!-- For phone search UI -->
+    <fragment
+        android:id="@+id/phone_number_picker_fragment"
+        class="com.android.contacts.list.PhoneNumberPickerFragment"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+</FrameLayout>
diff --git a/res/layout/directory_header.xml b/res/layout/directory_header.xml
index 63d8297..2748923 100644
--- a/res/layout/directory_header.xml
+++ b/res/layout/directory_header.xml
@@ -19,7 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     style="@style/DirectoryHeader"
     android:layout_width="match_parent"
-    android:layout_height="56dip"
+    android:layout_height="@dimen/directory_header_height"
     >
     <TextView
         android:id="@+id/count"
diff --git a/res/values-xlarge/dimens.xml b/res/values-xlarge/dimens.xml
index 2d39186..b2f2af1 100644
--- a/res/values-xlarge/dimens.xml
+++ b/res/values-xlarge/dimens.xml
@@ -31,4 +31,5 @@
     <dimen name="action_bar_search_spacing">12dip</dimen>
     <dimen name="shortcut_icon_size">64dip</dimen>
     <dimen name="list_section_height">37dip</dimen>
+    <dimen name="directory_header_height">56dip</dimen>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f369752..9525f54 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -135,4 +135,7 @@
 
     <!-- Margins for the group detail fragment divider in the header -->
     <dimen name="group_detail_divider_margin">15dip</dimen>
+
+    <!-- Height for directory headers in contact lists -->
+    <dimen name="directory_header_height">28dip</dimen>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6c0970f..8fd42b7 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -16,7 +16,6 @@
 <resources>
     <style name="DialtactsTheme" parent="android:Theme.Holo.Light">
         <item name="android:windowContentOverlay">@null</item>
-        <item name="list_item_height">?android:attr/listPreferredItemHeight</item>
         <item name="activated_background">@drawable/list_item_activated_background</item>
         <item name="section_header_background">@drawable/list_title_holo</item>
         <item name="list_section_header_height">32dip</item>
@@ -189,7 +188,7 @@
         <item name="contact_filter_popup_width">320dip</item>
     </style>
 
-    <style name="ContactPickerTheme" parent="@android:Theme">
+    <style name="ContactPickerTheme" parent="@android:Theme.Holo.Light">
         <item name="section_header_background">@drawable/section_header</item>
         <item name="list_item_divider">@drawable/list_item_divider</item>
         <item name="list_item_padding_top">4dip</item>
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index ed80eb1..a328b5b 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -27,11 +27,14 @@
 import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.list.DirectoryListLoader;
 import com.android.contacts.list.OnContactBrowserActionListener;
+import com.android.contacts.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.list.PhoneNumberPickerFragment;
 import com.android.contacts.list.StrequentContactListFragment;
 import com.android.contacts.preference.ContactsPreferenceActivity;
 import com.android.internal.telephony.ITelephony;
 
 import android.app.ActionBar;
+import android.app.ActionBar.LayoutParams;
 import android.app.ActionBar.Tab;
 import android.app.ActionBar.TabListener;
 import android.app.Activity;
@@ -49,10 +52,15 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Intents.UI;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.View;
+import android.widget.SearchView;
+import android.widget.SearchView.OnCloseListener;
+import android.widget.SearchView.OnQueryTextListener;
 
 /**
  * The dialer activity that has one tab with the virtual 12key
@@ -94,6 +102,80 @@
      */
     private int mLastManuallySelectedTab;
 
+    /**
+     * Fragment for searching phone numbers. Unlike the other Fragments, this doesn't correspond
+     * to tab but is shown by a search action.
+     */
+    private PhoneNumberPickerFragment mPhoneNumberPickerFragment;
+
+    private SearchView mSearchView;
+
+    /**
+     * True when this Activity is in its search UI (with a {@link SearchView} and
+     * {@link PhoneNumberPickerFragment}).
+     */
+    private boolean mInSearchUi;
+
+    /**
+     * Listener used when one of phone numbers in search UI is selected. This will initiate a
+     * phone call using the phone number.
+     */
+    private final OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener =
+            new OnPhoneNumberPickerActionListener() {
+                @Override
+                public void onPickPhoneNumberAction(Uri dataUri) {
+                    PhoneNumberInteraction.startInteractionForPhoneCall(
+                            DialtactsActivity.this, dataUri);
+                }
+
+                @Override
+                public void onShortcutIntentCreated(Intent intent) {
+                    Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
+                }
+    };
+
+    /**
+     * Listener used to send search queries to the phone search fragment.
+     */
+    private final OnQueryTextListener mPhoneSearchQueryTextListener =
+            new OnQueryTextListener() {
+                @Override
+                public boolean onQueryTextSubmit(String query) {
+                    // Ignore.
+                    return true;
+                }
+
+                @Override
+                public boolean onQueryTextChange(String newText) {
+                    // Show search result with non-empty text. Show a bare list otherwise.
+                    mPhoneNumberPickerFragment.setQueryString(newText, true);
+                    mPhoneNumberPickerFragment.setSearchMode(!TextUtils.isEmpty(newText));
+                    return true;
+                }
+    };
+
+    /**
+     * Listener used to handle the "close" button on the right side of {@link SearchView}.
+     * If some text is in the search view, this will clean it up. Otherwise this will exit
+     * the search UI and let users go back to usual Phone UI.
+     *
+     * This does _not_ handle back button.
+     *
+     * TODO: need "up" button instead of close button
+     */
+    private final OnCloseListener mPhoneSearchCloseListener =
+            new OnCloseListener() {
+                @Override
+                public boolean onClose() {
+                    if (TextUtils.isEmpty(mSearchView.getQuery())) {
+                        exitSearchUi();
+                    } else {
+                        mSearchView.setQuery(null, true);
+                    }
+                    return true;
+                }
+    };
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -112,6 +194,10 @@
                 .findFragmentById(R.id.contacts_fragment);
         mStrequentFragment = (StrequentContactListFragment) fragmentManager
                 .findFragmentById(R.id.favorites_fragment);
+        mPhoneNumberPickerFragment = (PhoneNumberPickerFragment) fragmentManager
+                .findFragmentById(R.id.phone_number_picker_fragment);
+        mPhoneNumberPickerFragment.setOnPhoneNumberPickerActionListener(
+                mPhoneNumberPickerActionListener);
 
         // Hide all tabs (the current tab will later be reshown once a tab is selected)
         final FragmentTransaction transaction = fragmentManager.beginTransaction();
@@ -119,6 +205,7 @@
         transaction.hide(mCallLogFragment);
         transaction.hide(mContactsFragment);
         transaction.hide(mStrequentFragment);
+        transaction.hide(mPhoneNumberPickerFragment);
         transaction.commit();
 
         // Setup the ActionBar tabs (the order matches the tab-index contants TAB_INDEX_*)
@@ -147,11 +234,16 @@
     protected void onPause() {
         super.onPause();
 
-        final int currentTabIndex = getActionBar().getSelectedTab().getPosition();
         final SharedPreferences.Editor editor =
                 getSharedPreferences(PREFS_DIALTACTS, MODE_PRIVATE).edit();
-        if (currentTabIndex == TAB_INDEX_CONTACTS || currentTabIndex == TAB_INDEX_FAVORITES) {
-            editor.putBoolean(PREF_FAVORITES_AS_CONTACTS, currentTabIndex == TAB_INDEX_FAVORITES);
+        // selectedTab becomes null in search UI.
+        final Tab selectedTab = getActionBar().getSelectedTab();
+        if (selectedTab != null) {
+            final int currentTabIndex = selectedTab.getPosition();
+            if (currentTabIndex == TAB_INDEX_CONTACTS || currentTabIndex == TAB_INDEX_FAVORITES) {
+                editor.putBoolean(
+                        PREF_FAVORITES_AS_CONTACTS, currentTabIndex == TAB_INDEX_FAVORITES);
+            }
         }
         editor.putInt(PREF_LAST_MANUALLY_SELECTED_TAB, mLastManuallySelectedTab);
 
@@ -381,7 +473,10 @@
 
     @Override
     public void onBackPressed() {
-        if (isTaskRoot()) {
+        if (mInSearchUi) {
+            // We should let the user go back to usual screens with tabs.
+            exitSearchUi();
+        } else if (isTaskRoot()) {
             // Instead of stopping, simply push this to the back of the stack.
             // This is only done when running at the top of the stack;
             // otherwise, we have been launched by someone else so need to
@@ -419,6 +514,7 @@
         @Override
         public void onTabSelected(Tab tab, FragmentTransaction ft) {
             ft.show(mFragment);
+            ft.hide(mPhoneNumberPickerFragment);
 
             // Remember this tab index. This function is also called, if the tab is set
             // automatically in which case the setter (setCurrentTab) has to set this to its old
@@ -543,4 +639,71 @@
             return super.onOptionsItemSelected(item);
         }
     }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery,
+            Bundle appSearchData, boolean globalSearch) {
+        if (mPhoneNumberPickerFragment != null && mPhoneNumberPickerFragment.isAdded()
+                && !globalSearch) {
+            enterSearchUi();
+        } else {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        }
+    }
+
+    /**
+     * Hides every tab and shows search UI for phone lookup.
+     */
+    private void enterSearchUi() {
+        final ActionBar actionBar = getActionBar();
+
+        final Tab tab = actionBar.getSelectedTab();
+        if (tab != null) {
+            mLastManuallySelectedTab = tab.getPosition();
+        }
+
+        // Instantiate or reset SearchView in ActionBar.
+        if (mSearchView == null) {
+            // TODO: layout is not what we want. Need "up" button instead of "close" button, etc.
+            final View searchViewLayout =
+                    getLayoutInflater().inflate(R.layout.custom_action_bar, null);
+            mSearchView = (SearchView) searchViewLayout.findViewById(R.id.search_view);
+            mSearchView.setQueryHint(getString(R.string.hint_findContacts));
+            mSearchView.setOnQueryTextListener(mPhoneSearchQueryTextListener);
+            mSearchView.setOnCloseListener(mPhoneSearchCloseListener);
+            mSearchView.requestFocus();
+            actionBar.setCustomView(searchViewLayout,
+                    new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+        } else {
+            mSearchView.setQuery(null, true);
+        }
+
+        actionBar.setDisplayShowCustomEnabled(true);
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+
+        // Show the search fragment and hide everything else.
+        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+        transaction.show(mPhoneNumberPickerFragment);
+        transaction.hide(mDialpadFragment);
+        transaction.hide(mCallLogFragment);
+        transaction.hide(mContactsFragment);
+        transaction.hide(mStrequentFragment);
+        transaction.commit();
+
+        mInSearchUi = true;
+    }
+
+    /**
+     * Goes back to usual Phone UI with tags. Previously selected Tag and associated Fragment
+     * should be automatically focused again.
+     */
+    private void exitSearchUi() {
+        final ActionBar actionBar = getActionBar();
+
+        // We want to hide SearchView and show Tabs. Also focus on previously selected one.
+        actionBar.setDisplayShowCustomEnabled(false);
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+        mInSearchUi = false;
+    }
 }
diff --git a/src/com/android/contacts/interactions/PhoneNumberInteraction.java b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
index 8430559..9762e3d 100644
--- a/src/com/android/contacts/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
@@ -282,13 +282,29 @@
      * Initiates the interaction. This may result in a phone call or sms message started
      * or a disambiguation dialog to determine which phone number should be used.
      */
-    public void startInteraction(Uri contactUri) {
+    @VisibleForTesting
+    /* package */ void startInteraction(Uri uri) {
         if (mLoader != null) {
             mLoader.reset();
         }
 
+        final Uri queryUri;
+        final String inputUriAsString = uri.toString();
+        if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
+            if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
+                queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+            } else {
+                queryUri = uri;
+            }
+        } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
+            queryUri = uri;
+        } else {
+            throw new UnsupportedOperationException(
+                    "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
+        }
+
         mLoader = new CursorLoader(mContext,
-                Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY),
+                queryUri,
                 PHONE_NUMBER_PROJECTION,
                 PHONE_NUMBER_SELECTION,
                 null,
@@ -356,29 +372,36 @@
     /**
      * Start call action using given contact Uri. If there are multiple candidates for the phone
      * call, dialog is automatically shown and the user is asked to choose one.
+     *
+     * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
+     * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
+     * data Uri won't.
      */
-    public static void startInteractionForPhoneCall(Activity activity, Uri contactUri) {
+    public static void startInteractionForPhoneCall(Activity activity, Uri uri) {
         (new PhoneNumberInteraction(activity, InteractionType.PHONE_CALL, null))
-                .startInteraction(contactUri);
+                .startInteraction(uri);
     }
 
     /**
      * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple
      * candidates for the phone call, dialog is automatically shown and the user is asked to choose
      * one.
+     *
+     * @param uri contact Uri (built from {@link Contacts#CONTENT_URI}) or data Uri
+     * (built from {@link Data#CONTENT_URI}). Contact Uri may show the disambiguation dialog while
+     * data Uri won't.
      */
-    public static void startInteractionForTextMessage(Activity activity, Uri contactUri) {
-        (new PhoneNumberInteraction(activity, InteractionType.SMS, null))
-                .startInteraction(contactUri);
+    public static void startInteractionForTextMessage(Activity activity, Uri uri) {
+        (new PhoneNumberInteraction(activity, InteractionType.SMS, null)).startInteraction(uri);
     }
 
     @VisibleForTesting
-    CursorLoader getLoader() {
+    /* package */ CursorLoader getLoader() {
         return mLoader;
     }
 
     @VisibleForTesting
-    void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
+    /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
         PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(),
                 phoneList, mInteractionType);
     }
diff --git a/src/com/android/contacts/list/ContactListItemView.java b/src/com/android/contacts/list/ContactListItemView.java
index 032b60f..df862a1 100644
--- a/src/com/android/contacts/list/ContactListItemView.java
+++ b/src/com/android/contacts/list/ContactListItemView.java
@@ -99,8 +99,33 @@
     private char[] mHighlightedPrefix;
 
     private int mDefaultPhotoViewSize;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+     * to align other data in this View.
+     */
     private int mPhotoViewWidth;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+     */
     private int mPhotoViewHeight;
+
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     * When true all the Views on the right side of the photo should have horizontal padding on
+     * those left assuming there is a photo.
+     */
+    private boolean mKeepHorizontalPaddingForPhotoView;
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     */
+    private boolean mKeepVerticalPaddingForPhotoView;
+
+    /**
+     * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+     * False indicates those values should be updated before being used in position calculation.
+     */
+    private boolean mPhotoViewWidthAndHeightAreReady = false;
+
     private int mLine1Height;
     private int mLine2Height;
     private int mLine3Height;
@@ -392,6 +417,9 @@
                     leftBound + mPhotoViewWidth,
                     photoTop + mPhotoViewHeight);
             leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+        } else if (mKeepHorizontalPaddingForPhotoView) {
+            // Draw nothing but keep the padding.
+            leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
         }
         return leftBound;
     }
@@ -459,7 +487,7 @@
      * Extracts width and height from the style
      */
     private void ensurePhotoViewSize() {
-        if (mPhotoViewWidth == 0 && mPhotoViewHeight == 0) {
+        if (!mPhotoViewWidthAndHeightAreReady) {
             if (mQuickContactEnabled) {
                 TypedArray a = mContext.obtainStyledAttributes(null,
                         com.android.internal.R.styleable.ViewGroup_Layout,
@@ -471,9 +499,15 @@
                         android.R.styleable.ViewGroup_Layout_layout_height,
                         ViewGroup.LayoutParams.WRAP_CONTENT);
                 a.recycle();
-            } else {
+            } else if (mPhotoView != null) {
                 mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+            } else {
+                final int defaultPhotoViewSize = getDefaultPhotoViewSize();
+                mPhotoViewWidth = mKeepHorizontalPaddingForPhotoView ? defaultPhotoViewSize : 0;
+                mPhotoViewHeight = mKeepVerticalPaddingForPhotoView ? defaultPhotoViewSize : 0;
             }
+
+            mPhotoViewWidthAndHeightAreReady = true;
         }
     }
 
@@ -567,6 +601,7 @@
             mQuickContact = new QuickContactBadge(mContext, null, QUICK_CONTACT_BADGE_STYLE);
             mQuickContact.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
             addView(mQuickContact);
+            mPhotoViewWidthAndHeightAreReady = false;
         }
         return mQuickContact;
     }
@@ -584,15 +619,30 @@
             // Quick contact style used above will set a background - remove it
             mPhotoView.setBackgroundDrawable(null);
             addView(mPhotoView);
+            mPhotoViewWidthAndHeightAreReady = false;
         }
         return mPhotoView;
     }
 
     /**
-     * Removes the photo view.  Should not be needed once we start handling different
-     * types of views as different types of views from the List's perspective.
+     * Removes the photo view.
      */
     public void removePhotoView() {
+        removePhotoView(false, true);
+    }
+
+    /**
+     * Removes the photo view.
+     *
+     * @param keepHorizontalPadding True means data on the right side will have padding on left,
+     * pretending there is still a photo view.
+     * @param keepVerticalPadding True means the View will have some height enough for
+     * accommodating a photo view.
+     */
+    public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+        mPhotoViewWidthAndHeightAreReady = false;
+        mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+        mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
         if (mPhotoView != null) {
             removeView(mPhotoView);
             mPhotoView = null;
@@ -821,6 +871,13 @@
                 getNameTextView(), displayOrder, highlightingEnabled, mHighlightedPrefix);
     }
 
+    public void hideDisplayName() {
+        if (mNameTextView != null) {
+            removeView(mNameTextView);
+            mNameTextView = null;
+        }
+    }
+
     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
         cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
         int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
@@ -831,6 +888,13 @@
         }
     }
 
+    public void hidePhoneticName() {
+        if (mPhoneticNameTextView != null) {
+            removeView(mPhoneticNameTextView);
+            mPhoneticNameTextView = null;
+        }
+    }
+
     /**
      * Sets the proper icon (star or presence or nothing)
      */
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
index 9356bb6..c159ac8 100644
--- a/src/com/android/contacts/list/PhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -103,6 +103,8 @@
         }
 
         loader.setUri(uri);
+
+        // TODO: we probably want to use default sort order in search mode.
         if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
             loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
         } else {
@@ -154,10 +156,46 @@
     @Override
     protected void bindView(View itemView, int partition, Cursor cursor, int position) {
         ContactListItemView view = (ContactListItemView)itemView;
+
+        // Look at elements before and after this position, checking if contact IDs are same.
+        // If they have one same contact ID, it means they can be grouped.
+        //
+        // In one group, only the first entry will show its photo and names (display name and
+        // phonetic name), and the other entries in the group show just their data (e.g. phone
+        // number, email address).
+        cursor.moveToPosition(position);
+        boolean isFirstEntry = true;
+        boolean showBottomDivider = true;
+        final long currentContactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+            final long previousContactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+            if (currentContactId == previousContactId) {
+                isFirstEntry = false;
+            }
+        }
+        cursor.moveToPosition(position);
+        if (cursor.moveToNext() && !cursor.isAfterLast()) {
+            final long nextContactId = cursor.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
+            if (currentContactId == nextContactId) {
+                // The following entry should be in the same group, which means we don't want a
+                // divider between them.
+                // TODO: we want a different divider than the divider between groups. Just hiding
+                // this divider won't be enough.
+                showBottomDivider = false;
+            }
+        }
+        cursor.moveToPosition(position);
+
         bindSectionHeaderAndDivider(view, position);
-        bindName(view, cursor);
-        bindPhoto(view, cursor);
+        if (isFirstEntry) {
+            bindName(view, cursor);
+            bindPhoto(view, cursor);
+        } else {
+            unbindName(view);
+            unbindPhoto(view);
+        }
         bindPhoneNumber(view, cursor);
+        view.setDividerVisible(showBottomDivider);
     }
 
     protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
@@ -190,6 +228,11 @@
         view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
     }
 
+    protected void unbindName(final ContactListItemView view) {
+        view.hideDisplayName();
+        view.hidePhoneticName();
+    }
+
     protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
         long photoId = 0;
         if (!cursor.isNull(PHONE_PHOTO_ID_COLUMN_INDEX)) {
@@ -198,4 +241,8 @@
 
         getPhotoLoader().loadPhoto(view.getPhotoView(), photoId);
     }
+
+    protected void unbindPhoto(final ContactListItemView view) {
+        view.removePhotoView(true, false);
+    }
 }
diff --git a/tests/src/com/android/contacts/interactions/PhoneNumberInteractionTest.java b/tests/src/com/android/contacts/interactions/PhoneNumberInteractionTest.java
index 2776a9f..d0e50c4 100644
--- a/tests/src/com/android/contacts/interactions/PhoneNumberInteractionTest.java
+++ b/tests/src/com/android/contacts/interactions/PhoneNumberInteractionTest.java
@@ -30,6 +30,7 @@
 import android.os.AsyncTask;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.Smoke;
@@ -103,6 +104,24 @@
         assertEquals("sms:123", intent.getDataString());
     }
 
+    public void testSendSmsWhenDataIdIsProvided() {
+        Uri dataUri = ContentUris.withAppendedId(Data.CONTENT_URI, 1);
+        expectQuery(dataUri, true /* isDataUri */ )
+                .returnRow(1, "987", 0, null, Phone.TYPE_HOME, null);
+
+        TestPhoneNumberInteraction interaction = new TestPhoneNumberInteraction(
+                mContext, InteractionType.SMS, null);
+
+        interaction.startInteraction(dataUri);
+        interaction.getLoader().waitForLoader();
+
+        Intent intent = mContext.getIntentForStartActivity();
+        assertNotNull(intent);
+
+        assertEquals(Intent.ACTION_SENDTO, intent.getAction());
+        assertEquals("sms:987", intent.getDataString());
+    }
+
     public void testSendSmsWhenThereIsPrimaryNumber() {
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, 13);
         expectQuery(contactUri)
@@ -186,7 +205,16 @@
     }
 
     private Query expectQuery(Uri contactUri) {
-        Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+        return expectQuery(contactUri, false);
+    }
+
+    private Query expectQuery(Uri uri, boolean isDataUri) {
+        final Uri dataUri;
+        if (isDataUri) {
+            dataUri = uri;
+        } else {
+            dataUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+        }
         return mContactsProvider
                 .expectQuery(dataUri)
                 .withProjection(