Two-partition list (Suggestions+A-Z list) for Join Contacts UI.
diff --git a/res/layout-finger/contacts_list_item.xml b/res/layout-finger/contacts_list_item.xml
index ca4f04f..b539bb0 100644
--- a/res/layout-finger/contacts_list_item.xml
+++ b/res/layout-finger/contacts_list_item.xml
@@ -22,11 +22,14 @@
     android:layout_height="wrap_content"
     android:orientation="vertical"
 >
-    <include 
-        android:id="@+id/header" 
-        layout="@layout/list_separator"
+    <TextView android:id="@+id/header"
+        style="?android:attr/listSeparatorTextViewStyle"
+        android:layout_width="fill_parent" 
+        android:layout_height="wrap_content"
+        android:background="#444444"
+        android:gravity="center"
     />
-    
+
     <RelativeLayout
         android:layout_width="fill_parent"
         android:layout_height="?android:attr/listPreferredItemHeight"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cfee718..cf41ef6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -114,6 +114,15 @@
     <!-- Menu item that joins an aggregate with another aggregate -->
     <string name="menu_joinAggregate">Join</string>
 
+    <!-- Activity title for the Join Contact list -->
+    <string name="titleJoinAggregate">Join contact</string>
+
+    <!-- List separator for the Join Contact list: Suggestions -->
+    <string name="separatorJoinAggregateSuggestions">Suggestions</string>
+
+    <!-- List separator for the Join Contact list: A-Z -->
+    <string name="separatorJoinAggregateAll">All contacts</string>
+
     <!-- Toast shown after two contacts have been joined by a user action -->
     <string name="contactsJoinedMessage">Contacts joined</string>
 
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 9c3081c..438133e 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -36,7 +36,9 @@
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Aggregates;
+import android.provider.ContactsContract.Aggregates.AggregationSuggestions;
 import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -107,8 +109,10 @@
 
     /**
      * The action for the join contact activity.
+     * <p>
+     * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
      *
-     * TODO: move to {@link Contacts}.
+     * TODO: move to {@link ContactsContract}.
      */
     public static final String JOIN_AGGREGATE =
             "com.android.contacts.action.JOIN_AGGREGATE";
@@ -164,13 +168,16 @@
     /** Run a search query in PICK mode, but that still launches to VIEW */
     // TODO Remove this mode if we decided it is really not needed.
     /*static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;*/
-    
+
     /** Show join suggestions followed by an A-Z list */
     static final int MODE_JOIN_AGGREGATE = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
             | MODE_MASK_NO_DATA;
-            
+
     static final int DEFAULT_MODE = MODE_ALL_CONTACTS;
 
+    /** Maximum number of suggestions shown for joining aggregates */
+    static final int MAX_SUGGESTIONS = 4;
+
     /**
      * The type of data to display in the main contacts list.
      */
@@ -469,6 +476,8 @@
                 setResult(RESULT_CANCELED);
                 finish();
             }
+
+            setTitle(R.string.titleJoinAggregate);
         }
 
         if (mMode == MODE_UNKNOWN) {
@@ -501,7 +510,11 @@
         // We manually save/restore the listview state
         list.setSaveEnabled(false);
 
-        mQueryHandler = new QueryHandler(this);
+        if (mMode == MODE_JOIN_AGGREGATE) {
+            mQueryHandler = new SuggestionsQueryHandler(this, mQueryAggregateId);
+        } else {
+            mQueryHandler = new QueryHandler(this);
+        }
         mJustCreated = true;
 
         // TODO(jham) redesign this
@@ -1263,11 +1276,14 @@
                 break;
 
             case MODE_JOIN_AGGREGATE:
-                mQueryHandler.startQuery(QUERY_TOKEN, null,
-                        Aggregates.CONTENT_URI, AGGREGATES_PROJECTION,
-                        Aggregates._ID + " != " + mQueryAggregateId, null,
-                        getSortOrder(AGGREGATES_PROJECTION));
-
+                Uri suggestionsUri = Aggregates.CONTENT_URI.buildUpon()
+                        .appendEncodedPath(String.valueOf(mQueryAggregateId))
+                        .appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY)
+                        .appendQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS,
+                                String.valueOf(MAX_SUGGESTIONS))
+                        .build();
+                mQueryHandler.startQuery(QUERY_TOKEN, null, suggestionsUri, AGGREGATES_PROJECTION,
+                        null, null, null);
                 break;
         }
     }
@@ -1464,8 +1480,8 @@
     }
     */
 
-    private static final class QueryHandler extends AsyncQueryHandler {
-        private final WeakReference<ContactsListActivity> mActivity;
+    private static class QueryHandler extends AsyncQueryHandler {
+        protected final WeakReference<ContactsListActivity> mActivity;
 
         public QueryHandler(Context context) {
             super(context.getContentResolver());
@@ -1495,6 +1511,46 @@
         }
     }
 
+    /**
+     * Query handler for the suggestions query used in the Join Contacts UI.  Once the
+     * suggestions query is complete, the handler launches an A-Z query.  The entire search is only
+     * done once the second query is complete.
+     */
+    private static final class SuggestionsQueryHandler extends QueryHandler {
+        boolean mSuggestionsQueryComplete;
+        private final long mAggregateId;
+
+        public SuggestionsQueryHandler(Context context, long aggregateId) {
+            super(context);
+            mAggregateId = aggregateId;
+        }
+
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            if (mSuggestionsQueryComplete) {
+                super.onQueryComplete(token, cookie, cursor);
+                return;
+            }
+
+            mSuggestionsQueryComplete = true;
+
+            final ContactsListActivity activity = mActivity.get();
+            if (activity != null && !activity.isFinishing()) {
+                if (cursor.getCount() > 0) {
+                    activity.mAdapter.setSuggestionsCursor(cursor);
+                } else {
+                    activity.mAdapter.setSuggestionsCursor(null);
+                }
+                startQuery(QUERY_TOKEN, null, Aggregates.CONTENT_URI, AGGREGATES_PROJECTION,
+                        Aggregates._ID + " != " + mAggregateId, null,
+                        getSortOrder(AGGREGATES_PROJECTION));
+
+            } else {
+                cursor.close();
+            }
+        }
+    }
+
     final static class ContactListItemCache {
         public TextView header;
         public TextView nameView;
@@ -1520,6 +1576,8 @@
         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
         private boolean mDisplaySectionHeaders = true;
         private int[] mSectionPositions;
+        private Cursor mSuggestionsCursor;
+        private int mSuggestionsCursorCount;
 
         public ContactItemListAdapter(Context context) {
             super(context, R.layout.contacts_list_item, null, false);
@@ -1567,6 +1625,14 @@
             }
         }
 
+        public void setSuggestionsCursor(Cursor cursor) {
+            if (mSuggestionsCursor != null) {
+                mSuggestionsCursor.close();
+            }
+            mSuggestionsCursor = cursor;
+            mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
+        }
+
         private SectionIndexer getNewIndexer(Cursor cursor) {
             /* if (Locale.getDefault().equals(Locale.JAPAN)) {
                 return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
@@ -1615,7 +1681,7 @@
 
         @Override
         public int getItemViewType(int position) {
-            if (position == mFrequentSeparatorPos) {
+            if (getSeparatorId(position) != 0) {
                 // We don't want the separator view to be recycled.
                 return IGNORE_ITEM_VIEW_TYPE;
             }
@@ -1630,30 +1696,56 @@
             }
 
             // Handle the separator specially
-            if (position == mFrequentSeparatorPos) {
+            int separatorId = getSeparatorId(position);
+            if (separatorId != 0) {
                 LayoutInflater inflater =
                         (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
-                view.setText(R.string.favoritesFrquentSeparator);
+                view.setText(separatorId);
                 return view;
             }
 
+            boolean showingSuggestion;
+            Cursor cursor;
+            if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
+                showingSuggestion = true;
+                cursor = mSuggestionsCursor;
+            } else {
+                showingSuggestion = false;
+                cursor = mCursor;
+            }
+
             int realPosition = getRealPosition(position);
-            if (!mCursor.moveToPosition(realPosition)) {
+            if (!cursor.moveToPosition(realPosition)) {
                 throw new IllegalStateException("couldn't move cursor to position " + position);
             }
 
             View v;
             if (convertView == null) {
-                v = newView(mContext, mCursor, parent);
+                v = newView(mContext, cursor, parent);
             } else {
                 v = convertView;
             }
-            bindView(v, mContext, mCursor);
-            bindSectionHeader(v, realPosition);
+            bindView(v, mContext, cursor);
+            bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
             return v;
         }
 
+        private int getSeparatorId(int position) {
+            int separatorId = 0;
+            if (position == mFrequentSeparatorPos) {
+                separatorId = R.string.favoritesFrquentSeparator;
+            }
+            if (mSuggestionsCursorCount != 0) {
+                if (position == 0) {
+                    separatorId = R.string.separatorJoinAggregateSuggestions;
+                } else if (position == mSuggestionsCursorCount + 1) {
+                    separatorId = R.string.separatorJoinAggregateAll;
+                }
+            }
+            return separatorId;
+        }
+
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             final View view = super.newView(context, cursor, parent);
@@ -1809,9 +1901,9 @@
             } */
         }
 
-        private void bindSectionHeader(View view, int position) {
+        private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
-            if (!mDisplaySectionHeaders) {
+            if (!displaySectionHeaders) {
                 cache.header.setVisibility(View.GONE);
             } else {
                 final int section = getSectionForPosition(position);
@@ -1914,7 +2006,8 @@
 
             int position = mSectionPositions[sectionIndex];
             if (position == ListView.INVALID_POSITION) {
-                position = mSectionPositions[sectionIndex] = mIndexer.getPositionForSection(sectionIndex);
+                position = mSectionPositions[sectionIndex] =
+                        mIndexer.getPositionForSection(sectionIndex);
             }
 
             return position;
@@ -1955,12 +2048,21 @@
 
         @Override
         public boolean isEnabled(int position) {
+            if (mSuggestionsCursorCount > 0) {
+                return position != 0 && position != mSuggestionsCursorCount + 1;
+            }
             return position != mFrequentSeparatorPos;
         }
 
         @Override
         public int getCount() {
-            if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
+            if (mSuggestionsCursorCount != 0) {
+                // When showing suggestions, we have 2 additional list items: the "Suggestions"
+                // and "All contacts" headers.
+                return mSuggestionsCursorCount + super.getCount() + 2;
+            }
+            else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
+                // When showing strequent list, we have an additional list item - the separator.
                 return super.getCount() + 1;
             } else {
                 return super.getCount();
@@ -1968,7 +2070,19 @@
         }
 
         private int getRealPosition(int pos) {
-            if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
+            if (mSuggestionsCursorCount != 0) {
+                // When showing suggestions, we have 2 additional list items: the "Suggestions"
+                // and "All contacts" separators.
+                if (pos < mSuggestionsCursorCount + 2) {
+                    // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
+                    // separator.
+                    return pos - 1;
+                } else {
+                    // We are in the lower partition (All contacts). Adjusting for the size
+                    // of the upper partition plus the two separators.
+                    return pos - mSuggestionsCursorCount - 2;
+                }
+            } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
                 // No separator, identity map
                 return pos;
             } else if (pos <= mFrequentSeparatorPos) {
@@ -1978,16 +2092,27 @@
                 // After the separator, remove 1 from the pos to get the real underlying pos
                 return pos - 1;
             }
-
         }
 
         @Override
         public Object getItem(int pos) {
-            return super.getItem(getRealPosition(pos));
+            if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
+                mSuggestionsCursor.moveToPosition(getRealPosition(pos));
+                return mSuggestionsCursor;
+            } else {
+                return super.getItem(getRealPosition(pos));
+            }
         }
 
         @Override
         public long getItemId(int pos) {
+            if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
+                if (mSuggestionsCursor.moveToPosition(pos - 1)) {
+                    return mSuggestionsCursor.getLong(mRowIDColumn);
+                } else {
+                    return 0;
+                }
+            }
             return super.getItemId(getRealPosition(pos));
         }
     }
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index c6ee7bf..c6fd7eb 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -526,7 +526,7 @@
 
     private void splitContact(long contactId) {
         setAggregationException(contactId, AggregationExceptions.TYPE_KEEP_OUT);
-        Toast.makeText(this, R.string.contactsSplitMessage, Toast.LENGTH_SHORT);
+        Toast.makeText(this, R.string.contactsSplitMessage, Toast.LENGTH_SHORT).show();
         mAdapter.notifyDataSetChanged();
     }
 
@@ -543,7 +543,7 @@
             c.close();
         }
 
-        Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_SHORT);
+        Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_SHORT).show();
         mAdapter.notifyDataSetChanged();
     }