Merge "Show commands for XMPP video chat for Google Talk"
diff --git a/res/drawable-hdpi/suggestion_bg.9.png b/res/drawable-hdpi/suggestion_bg.9.png
new file mode 100644
index 0000000..687cc08
--- /dev/null
+++ b/res/drawable-hdpi/suggestion_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/suggestion_bg.9.png b/res/drawable-mdpi/suggestion_bg.9.png
new file mode 100644
index 0000000..687cc08
--- /dev/null
+++ b/res/drawable-mdpi/suggestion_bg.9.png
Binary files differ
diff --git a/res/layout/aggregation_suggestions.xml b/res/layout/aggregation_suggestions.xml
new file mode 100644
index 0000000..684fe91
--- /dev/null
+++ b/res/layout/aggregation_suggestions.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 2010, 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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:background="@drawable/suggestion_bg">
+    <TextView
+        android:id="@+id/aggregation_suggestion_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"
+        android:layout_marginLeft="5dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textStyle="bold"
+    />
+
+    <LinearLayout
+        android:id="@+id/aggregation_suggestions"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+    />
+</LinearLayout>
diff --git a/res/layout/aggregation_suggestions_item.xml b/res/layout/aggregation_suggestions_item.xml
new file mode 100644
index 0000000..06d153e
--- /dev/null
+++ b/res/layout/aggregation_suggestions_item.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 2010, 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.
+ */
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.ui.widget.AggregationSuggestionView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingLeft="5dip"
+    android:paddingRight="15dip"
+>
+    <Button
+        android:id="@+id/aggregation_suggestion_join_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/aggregation_suggestion_join_button"
+        android:layout_centerInParent="true"
+        android:layout_alignParentRight="true"
+    />
+
+    <ImageView
+        android:id="@+id/aggregation_suggestion_photo"
+        android:layout_width="@dimen/aggregation_suggestion_icon_size"
+        android:layout_height="@dimen/aggregation_suggestion_icon_size"
+        android:layout_alignParentLeft="true"
+        android:layout_centerInParent="true"
+        android:layout_marginTop="4dip"
+        android:scaleType="fitCenter"
+    />
+
+    <TextView
+        android:id="@+id/aggregation_suggestion_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/aggregation_suggestion_photo"
+        android:layout_toLeftOf="@id/aggregation_suggestion_join_button"
+        android:layout_marginLeft="10dip"
+        android:layout_marginTop="4dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"
+    />
+
+    <TextView
+        android:id="@+id/aggregation_suggestion_data"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/aggregation_suggestion_photo"
+        android:layout_toLeftOf="@id/aggregation_suggestion_join_button"
+        android:layout_below="@id/aggregation_suggestion_name"
+        android:layout_marginLeft="10dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorSecondary"
+    />
+</view>
diff --git a/res/layout/item_contact_editor.xml b/res/layout/item_contact_editor.xml
index b75c82a..6886503 100644
--- a/res/layout/item_contact_editor.xml
+++ b/res/layout/item_contact_editor.xml
@@ -105,6 +105,13 @@
             android:layout_marginTop="6dip"
             android:layout_marginBottom="4dip" />
 
+        <ViewStub android:id="@+id/aggregation_suggestion_stub"
+            android:inflatedId="@+id/aggregation_suggestion"
+            android:layout="@layout/aggregation_suggestions"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:visibility="visible"/>
+
         <LinearLayout
             android:id="@+id/sect_fields"
             android:layout_width="match_parent"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3bc7ff6..ff9e92e 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -43,4 +43,6 @@
     <dimen name="list_item_header_chip_right_margin">4dip</dimen>
     <dimen name="list_item_header_checkbox_margin">5dip</dimen>
 
+    <dimen name="aggregation_suggestion_icon_size">40dip</dimen>
+
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e47eb18..100dcc4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1250,4 +1250,21 @@
 
     <!-- The name of the invisible local contact directory -->
     <string name="local_invisible_directory">Other</string>
+
+    <!-- The heading of the contact aggregation suggestions section in Contact editor. [CHAR LIMIT=128]-->
+    <plurals name="aggregation_suggestion_title">
+        <item quantity="one">Similar contact</item>
+        <item quantity="other">Similar contacts</item>
+    </plurals>
+
+    <!-- The heading of the aggregation suggestion section of the Contact editor
+        indicating the contact that will be joined with the current contact. [CHAR LIMIT=128]-->
+    <string name="aggregation_suggestion_joined_title">Joined contact:</string>
+
+    <!-- The button next to a contact aggregation suggestion in Contact editor. [CHAR LIMIT=12]-->
+    <string name="aggregation_suggestion_join_button">Join</string>
+
+    <!-- The button next to the message about multiple contact aggregation suggestions in Contact editor. [CHAR LIMIT=12]-->
+    <string name="aggregation_suggestion_view_button">View</string>
+
 </resources>
diff --git a/src/com/android/contacts/TwelveKeyDialer.java b/src/com/android/contacts/TwelveKeyDialer.java
index de53293..b9a65ae 100644
--- a/src/com/android/contacts/TwelveKeyDialer.java
+++ b/src/com/android/contacts/TwelveKeyDialer.java
@@ -303,6 +303,7 @@
         if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
             // see if we are "adding a call" from the InCallScreen; false by default.
             mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
+
             Uri uri = intent.getData();
             if (uri != null) {
                 if ("tel".equals(uri.getScheme())) {
@@ -326,10 +327,15 @@
                     }
                 }
             } else {
-                // Like ACTION_MAIN
-                // If there's already an active call, bring up an intermediate UI
-                // to make the user confirm what they really want to do.
-                needToShowDialpadChooser = phoneIsInUse();
+                // ACTION_DIAL or ACTION_VIEW with no data.
+                // This behaves basically like ACTION_MAIN: If there's
+                // already an active call, bring up an intermediate UI to
+                // make the user confirm what they really want to do.
+                // Be sure *not* to show the dialpad chooser if this is an
+                // explicit "Add call" action, though.
+                if (!mIsAddCallMode && phoneIsInUse()) {
+                    needToShowDialpadChooser = true;
+                }
             }
         } else if (Intent.ACTION_MAIN.equals(action)) {
             // The MAIN action means we're bringing up a blank dialer
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index de72b69..c09dc47 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -82,6 +82,7 @@
         }
 
         mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI);
+        parseSelectedContactUri();
     }
 
     @Override
@@ -135,24 +136,7 @@
                 || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) {
             mSelectedContactUri = uri;
 
-            if (mSelectedContactUri != null) {
-                if (!mSelectedContactUri.toString()
-                        .startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
-                    throw new IllegalStateException(
-                            "Contact list contains a non-lookup URI: " + mSelectedContactUri);
-                }
-
-                String directoryParam =
-                    mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
-                mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam)
-                        ? Directory.DEFAULT
-                        : Long.parseLong(directoryParam);
-                mSelectedContactLookupKey =
-                        Uri.encode(mSelectedContactUri.getPathSegments().get(2));
-            } else {
-                mSelectedContactDirectoryId = Directory.DEFAULT;
-                mSelectedContactLookupKey = null;
-            }
+            parseSelectedContactUri();
 
             // Configure the adapter to show the selection based on the lookup key extracted
             // from the URI
@@ -163,6 +147,27 @@
         }
     }
 
+    private void parseSelectedContactUri() {
+        if (mSelectedContactUri != null) {
+            if (!mSelectedContactUri.toString()
+                    .startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
+                throw new IllegalStateException(
+                        "Contact list contains a non-lookup URI: " + mSelectedContactUri);
+            }
+
+            String directoryParam =
+                mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+            mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam)
+                    ? Directory.DEFAULT
+                    : Long.parseLong(directoryParam);
+            mSelectedContactLookupKey =
+                    Uri.encode(mSelectedContactUri.getPathSegments().get(2));
+        } else {
+            mSelectedContactDirectoryId = Directory.DEFAULT;
+            mSelectedContactLookupKey = null;
+        }
+    }
+
     @Override
     protected void configureAdapter() {
         super.configureAdapter();
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
index aaf80f8..ef0807a 100644
--- a/src/com/android/contacts/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -222,20 +222,17 @@
 
         // TODO preserve the order of partition to match those of the cursor
         // Phase I: add new directories
-        try {
-            while (cursor.moveToNext()) {
-                long id = cursor.getLong(idColumnIndex);
-                directoryIds.add(id);
-                if (getPartitionByDirectoryId(id) == -1) {
-                    DirectoryPartition partition = new DirectoryPartition(false, true);
-                    partition.setDirectoryId(id);
-                    partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
-                    partition.setDisplayName(cursor.getString(displayNameColumnIndex));
-                    addPartition(partition);
-                }
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            long id = cursor.getLong(idColumnIndex);
+            directoryIds.add(id);
+            if (getPartitionByDirectoryId(id) == -1) {
+                DirectoryPartition partition = new DirectoryPartition(false, true);
+                partition.setDirectoryId(id);
+                partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
+                partition.setDisplayName(cursor.getString(displayNameColumnIndex));
+                addPartition(partition);
             }
-        } finally {
-            cursor.close();
         }
 
         // Phase II: remove deleted directories
@@ -257,10 +254,7 @@
     @Override
     public void changeCursor(int partitionIndex, Cursor cursor) {
         if (partitionIndex >= getPartitionCount()) {
-            // There is no partition for this data - just drop it on the ground
-            if (cursor != null) {
-                cursor.close();
-            }
+            // There is no partition for this data
             return;
         }
 
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/EntityDeltaList.java
index f1af387..e68b6ef 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/EntityDeltaList.java
@@ -34,6 +34,7 @@
 
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 
 /**
  * Container for multiple {@link EntityDelta} objects, usually when editing
@@ -42,6 +43,7 @@
  */
 public class EntityDeltaList extends ArrayList<EntityDelta> implements Parcelable {
     private boolean mSplitRawContacts;
+    private List<Long> mJoinWithRawContactIds;
 
     private EntityDeltaList() {
     }
@@ -142,6 +144,22 @@
             backRefs[rawContactIndex++] = firstBatch;
             delta.buildDiff(diff);
 
+            // If the user chose to join with some other existing raw contact(s) at save time,
+            // add aggregation exceptions for all those raw contacts.
+            if (mJoinWithRawContactIds != null) {
+                for (Long joinedRawContactId : mJoinWithRawContactIds) {
+                    final Builder builder = beginKeepTogether();
+                    builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
+                    if (rawContactId != -1) {
+                        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
+                    } else {
+                        builder.withValueBackReference(
+                                AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                    }
+                    diff.add(builder.build());
+                }
+            }
+
             // Only create rules for inserts
             if (!delta.isContactInsert()) continue;
 
@@ -313,6 +331,10 @@
         mSplitRawContacts = true;
     }
 
+    public void setJoinWithRawContacts(List<Long> rawContactIds) {
+        mJoinWithRawContactIds = rawContactIds;
+    }
+
     /** {@inheritDoc} */
     public int describeContents() {
         // Nothing special about this parcel
@@ -326,14 +348,17 @@
         for (EntityDelta delta : this) {
             dest.writeParcelable(delta, flags);
         }
+        dest.writeList(mJoinWithRawContactIds);
     }
 
+    @SuppressWarnings("unchecked")
     public void readFromParcel(Parcel source) {
         final ClassLoader loader = getClass().getClassLoader();
         final int size = source.readInt();
         for (int i = 0; i < size; i++) {
             this.add(source.<EntityDelta> readParcelable(loader));
         }
+        mJoinWithRawContactIds = source.readArrayList(loader);
     }
 
     public static final Parcelable.Creator<EntityDeltaList> CREATOR =
diff --git a/src/com/android/contacts/ui/widget/AggregationSuggestionView.java b/src/com/android/contacts/ui/widget/AggregationSuggestionView.java
new file mode 100644
index 0000000..e03f78b
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/AggregationSuggestionView.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 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.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.views.editor.AggregationSuggestionEngine.Suggestion;
+
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * A view that contains a name, picture and other data for a contact aggregation suggestion.
+ */
+public class AggregationSuggestionView extends RelativeLayout implements OnClickListener {
+
+    public interface Listener {
+
+        /**
+         * Callback that passes the contact ID to join with and, for convenience,
+         * also the list of constituent raw contact IDs to avoid a separate query
+         * for those.
+         */
+        public void onJoinAction(long contactId, List<Long> rawContacIds);
+    }
+
+    private Listener mListener;
+    private long mContactId;
+    private List<Long> mRawContactIds;
+
+    public AggregationSuggestionView(Context context) {
+        super(context);
+    }
+
+    public AggregationSuggestionView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AggregationSuggestionView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void bindSuggestion(Suggestion suggestion, boolean showJoinButton) {
+        mContactId = suggestion.contactId;
+        mRawContactIds = suggestion.rawContactIds;
+        ImageView photo = (ImageView) findViewById(R.id.aggregation_suggestion_photo);
+        if (suggestion.photo != null) {
+            photo.setImageBitmap(BitmapFactory.decodeByteArray(
+                    suggestion.photo, 0, suggestion.photo.length));
+        } else {
+            photo.setImageResource(R.drawable.ic_contact_picture_2);
+        }
+
+        TextView name = (TextView) findViewById(R.id.aggregation_suggestion_name);
+        name.setText(suggestion.name);
+
+        TextView data = (TextView) findViewById(R.id.aggregation_suggestion_data);
+        String dataText = null;
+        if (suggestion.nickname != null) {
+            dataText = suggestion.nickname;
+        } else if (suggestion.emailAddress != null) {
+            dataText = suggestion.emailAddress;
+        } else if (suggestion.phoneNumber != null) {
+            dataText = suggestion.phoneNumber;
+        }
+        data.setText(dataText);
+
+        Button join = (Button) findViewById(R.id.aggregation_suggestion_join_button);
+        if (showJoinButton) {
+            join.setOnClickListener(this);
+            join.setVisibility(View.VISIBLE);
+        } else {
+            join.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mListener != null) {
+            mListener.onJoinAction(mContactId, mRawContactIds);
+        }
+    }
+}
diff --git a/src/com/android/contacts/ui/widget/BaseContactEditorView.java b/src/com/android/contacts/ui/widget/BaseContactEditorView.java
index c2f9136..7ee2dac 100644
--- a/src/com/android/contacts/ui/widget/BaseContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/BaseContactEditorView.java
@@ -97,9 +97,4 @@
      * apply to that state.
      */
     public abstract void setState(EntityDelta state, ContactsSource source, ViewIdGenerator vig);
-
-    /**
-     * Sets the {@link EditorListener} on the name field
-     */
-    public abstract void setNameEditorListener(EditorListener listener);
 }
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 3a8b3b3..15d1726 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -200,12 +200,8 @@
         }
     }
 
-    /**
-     * Sets the {@link EditorListener} on the name field
-     */
-    @Override
-    public void setNameEditorListener(EditorListener listener) {
-        mName.setEditorListener(listener);
+    public GenericEditorView getNameEditor() {
+        return mName;
     }
 
     @Override
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index 612fff4..80982bc 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -434,6 +434,10 @@
         if (mMoreOrLess != null) mMoreOrLess.setEnabled(enabled);
     }
 
+    public ValuesDelta getValues() {
+        return mEntry;
+    }
+
     /**
      * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before
      * and after the input text is removed.
diff --git a/src/com/android/contacts/ui/widget/ReadOnlyContactEditorView.java b/src/com/android/contacts/ui/widget/ReadOnlyContactEditorView.java
index 4635f6a..011bcb1 100644
--- a/src/com/android/contacts/ui/widget/ReadOnlyContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ReadOnlyContactEditorView.java
@@ -191,14 +191,6 @@
         }
     }
 
-    /**
-     * Sets the {@link EditorListener} on the name field
-     */
-    @Override
-    public void setNameEditorListener(EditorListener listener) {
-        // do nothing
-    }
-
     @Override
     public long getRawContactId() {
         return mRawContactId;
diff --git a/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java
new file mode 100644
index 0000000..11f10f9
--- /dev/null
+++ b/src/com/android/contacts/views/editor/AggregationSuggestionEngine.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2010 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.views.editor;
+
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions.Builder;
+import android.provider.ContactsContract.Data;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode.
+ */
+public class AggregationSuggestionEngine extends HandlerThread {
+    public static final String TAG = "AggregationSuggestionEngine";
+
+    private static final int MESSAGE_NAME_CHANGE = 1;
+    private static final int MESSAGE_DATA_CURSOR = 2;
+
+    private static final long SUGGESTION_LOOKUP_DELAY_MILLIS = 300;
+
+    private static final int MAX_SUGGESTION_COUNT = 3;
+
+    private final Context mContext;
+
+    private long[] mSuggestedContactIds = new long[0];
+
+    private Handler mMainHandler;
+    private Handler mHandler;
+    private long mContactId;
+    private Listener mListener;
+    private Cursor mDataCursor;
+
+    public interface Listener {
+        void onAggregationSuggestionChange();
+    }
+
+    public static final class Suggestion {
+        public long contactId;
+        public List<Long> rawContactIds;
+        public String lookupKey;
+        public String name;
+        public String phoneNumber;
+        public String emailAddress;
+        public String nickname;
+        public byte[] photo;
+
+        @Override
+        public String toString() {
+            return "ID: " + contactId + " rawContactIds: " + rawContactIds + " name: " + name
+                    + " phone: " + phoneNumber + " email: " + emailAddress + " nickname: "
+                    + nickname + (photo != null ? " [has photo]" : "");
+        }
+    }
+
+    public AggregationSuggestionEngine(Context context) {
+        super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND);
+        mContext = context;
+        mMainHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                AggregationSuggestionEngine.this.deliverNotification((Cursor) msg.obj);
+            }
+        };
+    }
+
+    protected Handler getHandler() {
+        if (mHandler == null) {
+            mHandler = new Handler(getLooper()) {
+                @Override
+                public void handleMessage(Message msg) {
+                    AggregationSuggestionEngine.this.handleMessage(msg);
+                }
+            };
+        }
+        return mHandler;
+    }
+
+    public void setContactId(long contactId) {
+        mContactId = contactId;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public boolean quit() {
+        if (mDataCursor != null) {
+            mDataCursor.close();
+        }
+        mDataCursor = null;
+        return super.quit();
+    }
+
+    public void onNameChange(ValuesDelta values) {
+        Handler handler = getHandler();
+        handler.removeMessages(MESSAGE_NAME_CHANGE);
+
+        Uri uri = buildAggregationSuggestionUri(values);
+        if (uri == null) {
+            return;
+        }
+
+        Message msg = handler.obtainMessage(MESSAGE_NAME_CHANGE, uri);
+        handler.sendMessageDelayed(msg, SUGGESTION_LOOKUP_DELAY_MILLIS);
+    }
+
+    private Uri buildAggregationSuggestionUri(ValuesDelta values) {
+        StringBuilder nameSb = new StringBuilder();
+        appendValue(nameSb, values, StructuredName.PREFIX);
+        appendValue(nameSb, values, StructuredName.GIVEN_NAME);
+        appendValue(nameSb, values, StructuredName.MIDDLE_NAME);
+        appendValue(nameSb, values, StructuredName.FAMILY_NAME);
+        appendValue(nameSb, values, StructuredName.SUFFIX);
+
+        StringBuilder phoneticNameSb = new StringBuilder();
+        appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME);
+        appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME);
+        appendValue(phoneticNameSb, values, StructuredName.PHONETIC_GIVEN_NAME);
+
+        if (nameSb.length() == 0 && phoneticNameSb.length() == 0) {
+            return null;
+        }
+
+        Builder builder = AggregationSuggestions.builder()
+                .setLimit(MAX_SUGGESTION_COUNT)
+                .setContactId(mContactId);
+
+        if (nameSb.length() != 0) {
+            builder.addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, nameSb.toString());
+        }
+
+        if (phoneticNameSb.length() != 0) {
+            builder.addParameter(
+                    AggregationSuggestions.PARAMETER_MATCH_NAME, phoneticNameSb.toString());
+        }
+
+        return builder.build();
+    }
+
+    private void appendValue(StringBuilder sb, ValuesDelta values, String column) {
+        String value = values.getAsString(column);
+        if (!TextUtils.isEmpty(value)) {
+            if (sb.length() > 0) {
+                sb.append(' ');
+            }
+            sb.append(value);
+        }
+    }
+
+    protected void handleMessage(Message msg) {
+        switch(msg.what) {
+            case MESSAGE_NAME_CHANGE:
+                loadAggregationSuggestions((Uri) msg.obj);
+                break;
+        }
+    }
+
+    private static final class DataQuery {
+
+        public static final String SELECTION_PREFIX =
+                Data.MIMETYPE + " IN ('"
+                    + Phone.CONTENT_ITEM_TYPE + "','"
+                    + Email.CONTENT_ITEM_TYPE + "','"
+                    + StructuredName.CONTENT_ITEM_TYPE + "','"
+                    + Nickname.CONTENT_ITEM_TYPE + "','"
+                    + Photo.CONTENT_ITEM_TYPE + "')"
+                + " AND " + Data.CONTACT_ID + " IN (";
+
+        public static final String[] COLUMNS = {
+            Data._ID,
+            Data.CONTACT_ID,
+            Data.LOOKUP_KEY,
+            Data.PHOTO_ID,
+            Data.DISPLAY_NAME,
+            Data.RAW_CONTACT_ID,
+            Data.MIMETYPE,
+            Data.DATA1,
+            Data.IS_SUPER_PRIMARY,
+            Photo.PHOTO,
+        };
+
+        public static final int ID = 0;
+        public static final int CONTACT_ID = 1;
+        public static final int LOOKUP_KEY = 2;
+        public static final int PHOTO_ID = 3;
+        public static final int DISPLAY_NAME = 4;
+        public static final int RAW_CONTACT_ID = 5;
+        public static final int MIMETYPE = 6;
+        public static final int DATA1 = 7;
+        public static final int IS_SUPERPRIMARY = 8;
+        public static final int PHOTO = 9;
+    }
+
+    private void loadAggregationSuggestions(Uri uri) {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null);
+        try {
+            // If a new request is pending, chuck the result of the previous request
+            if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) {
+                return;
+            }
+
+            boolean changed = updateSuggestedContactIds(cursor);
+            if (!changed) {
+                return;
+            }
+
+            StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX);
+            int count = mSuggestedContactIds.length;
+            for (int i = 0; i < count; i++) {
+                if (i > 0) {
+                    sb.append(',');
+                }
+                sb.append(mSuggestedContactIds[i]);
+            }
+            sb.append(')');
+            sb.toString();
+
+            Cursor dataCursor = contentResolver.query(Data.CONTENT_URI,
+                    DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID);
+            mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private boolean updateSuggestedContactIds(Cursor cursor) {
+        int count = cursor.getCount();
+        boolean changed = count != mSuggestedContactIds.length;
+        if (!changed) {
+            while (cursor.moveToNext()) {
+                long contactId = cursor.getLong(0);
+                if (Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) {
+                    changed = true;
+                    break;
+                }
+            }
+        }
+
+        if (changed) {
+            mSuggestedContactIds = new long[count];
+            cursor.moveToPosition(-1);
+            for (int i = 0; i < count; i++) {
+                cursor.moveToNext();
+                mSuggestedContactIds[i] = cursor.getLong(0);
+            }
+            Arrays.sort(mSuggestedContactIds);
+        }
+
+        return changed;
+    }
+
+    protected void deliverNotification(Cursor dataCursor) {
+        if (mDataCursor != null) {
+            mDataCursor.close();
+        }
+        mDataCursor = dataCursor;
+        if (mListener != null) {
+            mListener.onAggregationSuggestionChange();
+        }
+    }
+
+    public int getSuggestedContactCount() {
+        return mSuggestedContactIds.length;
+    }
+
+    public List<Suggestion> getSuggestions() {
+        ArrayList<Suggestion> list = Lists.newArrayList();
+        if (mDataCursor != null) {
+            Suggestion suggestion = null;
+            long currentContactId = -1;
+            mDataCursor.moveToPosition(-1);
+            while (mDataCursor.moveToNext()) {
+                long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
+                if (contactId != currentContactId) {
+                    suggestion = new Suggestion();
+                    suggestion.contactId = contactId;
+                    suggestion.name = mDataCursor.getString(DataQuery.DISPLAY_NAME);
+                    suggestion.rawContactIds = Lists.newArrayList();
+                    list.add(suggestion);
+                    currentContactId = contactId;
+                }
+
+                Long rawContactId = Long.valueOf(mDataCursor.getLong(DataQuery.RAW_CONTACT_ID));
+                if (!suggestion.rawContactIds.contains(rawContactId)) {
+                    suggestion.rawContactIds.add(rawContactId);
+                }
+
+                String mimetype = mDataCursor.getString(DataQuery.MIMETYPE);
+                if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    String data = mDataCursor.getString(DataQuery.DATA1);
+                    int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
+                    if (!TextUtils.isEmpty(data)
+                            && (superprimary != 0 || suggestion.phoneNumber == null)) {
+                        suggestion.phoneNumber = data;
+                    }
+                } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    String data = mDataCursor.getString(DataQuery.DATA1);
+                    int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
+                    if (!TextUtils.isEmpty(data)
+                            && (superprimary != 0 || suggestion.emailAddress == null)) {
+                        suggestion.emailAddress = data;
+                    }
+                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    String data = mDataCursor.getString(DataQuery.DATA1);
+                    if (!TextUtils.isEmpty(data)) {
+                        suggestion.nickname = data;
+                    }
+                } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    long dataId = mDataCursor.getLong(DataQuery.ID);
+                    long photoId = mDataCursor.getLong(DataQuery.PHOTO_ID);
+                    if (dataId == photoId && !mDataCursor.isNull(DataQuery.PHOTO)) {
+                        suggestion.photo = mDataCursor.getBlob(DataQuery.PHOTO);
+                    }
+                }
+            }
+        }
+        return list;
+    }
+}
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
index 24c30d4..681931c 100644
--- a/src/com/android/contacts/views/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -19,21 +19,26 @@
 import com.android.contacts.JoinContactActivity;
 import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.Editor;
+import com.android.contacts.model.Editor.EditorListener;
 import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.GoogleSource;
 import com.android.contacts.model.Sources;
-import com.android.contacts.model.ContactsSource.EditType;
-import com.android.contacts.model.Editor.EditorListener;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.ViewIdGenerator;
+import com.android.contacts.ui.widget.AggregationSuggestionView;
 import com.android.contacts.ui.widget.BaseContactEditorView;
+import com.android.contacts.ui.widget.ContactEditorView;
+import com.android.contacts.ui.widget.GenericEditorView;
 import com.android.contacts.ui.widget.PhotoEditorView;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.WeakAsyncTask;
 import com.android.contacts.views.ContactLoader;
+import com.android.contacts.views.editor.AggregationSuggestionEngine.Suggestion;
+import com.google.android.collect.Lists;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -44,6 +49,7 @@
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -54,9 +60,9 @@
 import android.content.Intent;
 import android.content.Loader;
 import android.content.OperationApplicationException;
-import android.content.ContentProviderOperation.Builder;
 import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.Rect;
 import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.os.Bundle;
@@ -64,12 +70,13 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.provider.ContactsContract;
-import android.provider.MediaStore;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.MediaStore;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
@@ -78,9 +85,12 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewStub;
 import android.widget.ArrayAdapter;
 import android.widget.LinearLayout;
 import android.widget.ListAdapter;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import java.io.File;
@@ -89,10 +99,12 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
 
 public class ContactEditorFragment extends Fragment implements
         SplitContactConfirmationDialogFragment.Listener, PickPhotoDialogFragment.Listener,
-        SelectAccountDialogFragment.Listener {
+        SelectAccountDialogFragment.Listener, AggregationSuggestionEngine.Listener {
 
     private static final String TAG = "ContactEditorFragment";
 
@@ -145,6 +157,17 @@
     private static final File PHOTO_DIR = new File(
             Environment.getExternalStorageDirectory() + "/DCIM/Camera");
 
+    /**
+     * A delay in milliseconds used for bringing aggregation suggestions to
+     * the visible part of the screen. The reason this has to be done after
+     * a delay is a race condition with the soft keyboard.  The keyboard
+     * may expand to display its own autocomplete suggestions, which will
+     * reduce the visible area of the screen.  We will yield to the keyboard
+     * hoping that the delay is sufficient.  If not - part of the
+     * suggestion will be hidden, which is not fatal.
+     */
+    private static final int AGGREGATION_SUGGESTION_SCROLL_DELAY = 200;
+
     private File mCurrentPhotoFile;
 
     private Context mContext;
@@ -165,6 +188,8 @@
 
     private long mLoaderStartTime;
 
+    private AggregationSuggestionEngine mAggregationSuggestionEngine;
+
     public ContactEditorFragment() {
     }
 
@@ -175,6 +200,14 @@
     }
 
     @Override
+    public void onStop() {
+        super.onStop();
+        if (mAggregationSuggestionEngine != null) {
+            mAggregationSuggestionEngine.quit();
+        }
+    }
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
 
@@ -372,6 +405,29 @@
 
             mContent.addView(editor);
             editor.setState(entity, source, mViewIdGenerator);
+
+            if (editor instanceof ContactEditorView) {
+                final ContactEditorView rawContactEditor = (ContactEditorView) editor;
+                GenericEditorView nameEditor = rawContactEditor.getNameEditor();
+                nameEditor.setEditorListener(new EditorListener() {
+
+                    @Override
+                    public void onRequest(int request) {
+                        acquireAggregationSuggestions(rawContactEditor);
+                    }
+
+                    @Override
+                    public void onDeleted(Editor editor) {
+                    }
+                });
+
+                // If the user has already decided to join with a specific contact,
+                // trigger a refresh of the aggregation suggestion view to redisplay
+                // the selection.
+                if (mContactIdForJoin != 0) {
+                    acquireAggregationSuggestions(rawContactEditor);
+                }
+            }
         }
 
         // Show editor now that we've loaded state
@@ -555,7 +611,7 @@
     }
 
     /**
-     * Asynchonously saves the changes made by the user. This can be called even if nothing
+     * Asynchronously saves the changes made by the user. This can be called even if nothing
      * has changed
      */
     public void save(boolean closeAfterSave) {
@@ -953,6 +1009,163 @@
         }
     }
 
+    /**
+     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
+     */
+    protected long getContactId() {
+        for (EntityDelta rawContact : mState) {
+            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
+            if (contactId != null) {
+                return contactId;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Triggers an asynchronous search for aggregation suggestions.
+     */
+    public void acquireAggregationSuggestions(ContactEditorView rawContactEditor) {
+        if (mAggregationSuggestionEngine == null) {
+            mAggregationSuggestionEngine = new AggregationSuggestionEngine(getActivity());
+            mAggregationSuggestionEngine.setContactId(getContactId());
+            mAggregationSuggestionEngine.setListener(this);
+            mAggregationSuggestionEngine.start();
+        }
+
+        GenericEditorView nameEditor = rawContactEditor.getNameEditor();
+        mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
+    }
+
+    @Override
+    public void onAggregationSuggestionChange() {
+        // We may have chosen some contact to join with but then changed the contact name.
+        // Let's drop the obsolete decision to join.
+        setSelectedAggregationSuggestion(0, null);
+        updateAggregationSuggestionView();
+    }
+
+    protected void updateAggregationSuggestionView() {
+        ViewStub stub = (ViewStub)mContent.findViewById(R.id.aggregation_suggestion_stub);
+        if (stub != null) {
+            stub.inflate();
+            final View view = mContent.findViewById(R.id.aggregation_suggestion);
+            mContent.postDelayed(new Runnable() {
+
+                @Override
+                public void run() {
+                    requestAggregationSuggestionOnScreen(view);
+                }
+            }, AGGREGATION_SUGGESTION_SCROLL_DELAY);
+        }
+
+        View view = mContent.findViewById(R.id.aggregation_suggestion);
+
+        List<Suggestion> suggestions = null;
+        int count = mAggregationSuggestionEngine.getSuggestedContactCount();
+        if (count > 0) {
+            suggestions = mAggregationSuggestionEngine.getSuggestions();
+
+            if (mContactIdForJoin != 0) {
+                Suggestion chosenSuggestion = null;
+                for (Suggestion suggestion : suggestions) {
+                    if (suggestion.contactId == mContactIdForJoin) {
+                        chosenSuggestion = suggestion;
+                        break;
+                    }
+                }
+
+                if (chosenSuggestion != null) {
+                    suggestions = Lists.newArrayList(chosenSuggestion);
+                } else {
+                    // If the contact we wanted to join with is no longer suggested,
+                    // forget our decision to join with it.
+                    setSelectedAggregationSuggestion(0, null);
+                }
+            }
+
+            count = suggestions.size();
+        }
+
+        if (count == 0) {
+            view.setVisibility(View.GONE);
+            return;
+        }
+
+        TextView title = (TextView) view.findViewById(R.id.aggregation_suggestion_title);
+        if (mContactIdForJoin != 0) {
+            title.setText(R.string.aggregation_suggestion_joined_title);
+        } else {
+            title.setText(getActivity().getResources().getQuantityString(
+                    R.plurals.aggregation_suggestion_title, count));
+        }
+
+        LinearLayout itemList = (LinearLayout) view.findViewById(R.id.aggregation_suggestions);
+        itemList.removeAllViews();
+
+        LayoutInflater inflater = getActivity().getLayoutInflater();
+
+        for (Suggestion suggestion : suggestions) {
+            AggregationSuggestionView suggestionView =
+                    (AggregationSuggestionView) inflater.inflate(
+                            R.layout.aggregation_suggestions_item, null);
+            suggestionView.setLayoutParams(
+                    new LinearLayout.LayoutParams(
+                            LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+            suggestionView.setListener(new AggregationSuggestionView.Listener() {
+
+                @Override
+                public void onJoinAction(long contactId, List<Long> rawContactIds) {
+                    mState.setJoinWithRawContacts(rawContactIds);
+                    // If we are in the edit mode (indicated by a non-zero contact ID),
+                    // join the suggested contact, save all changes, and stay in the editor.
+                    if (getContactId() != 0) {
+                        doSaveAction(SaveMode.RELOAD);
+                    } else {
+                        mContactIdForJoin = contactId;
+                        updateAggregationSuggestionView();
+                    }
+                }
+            });
+            suggestionView.bindSuggestion(suggestion, mContactIdForJoin == 0 /* showJoinButton */);
+            itemList.addView(suggestionView);
+        }
+        view.setVisibility(View.VISIBLE);
+    }
+
+    /**
+     * Scrolls the editor if necessary to reveal the aggregation suggestion that is
+     * shown below the name editor. Makes sure that the currently focused field
+     * remains visible.
+     */
+    private void requestAggregationSuggestionOnScreen(final View view) {
+        Rect rect = getRelativeBounds(mContent, view);
+        View focused = mContent.findFocus();
+        if (focused != null) {
+            rect.union(getRelativeBounds(mContent, focused));
+        }
+        mContent.requestRectangleOnScreen(rect);
+    }
+
+    /**
+     * Computes bounds of the supplied view relative to its ascendant.
+     */
+    private Rect getRelativeBounds(View ascendant, View view) {
+        Rect rect = new Rect();
+        rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+
+        View parent = (View) view.getParent();
+        while (parent != ascendant) {
+            rect.offset(parent.getLeft(), parent.getTop());
+            parent = (View) parent.getParent();
+        }
+        return rect;
+    }
+
+    protected void setSelectedAggregationSuggestion(long contactId, List<Long> rawContactIds) {
+        mContactIdForJoin = contactId;
+        mState.setJoinWithRawContacts(rawContactIds);
+    }
 
     // TODO: There has to be a nicer way than this WeakAsyncTask...? Maybe call a service?
     /**
diff --git a/src/com/android/contacts/widget/CompositeCursorAdapter.java b/src/com/android/contacts/widget/CompositeCursorAdapter.java
index 6465a21..c6aa775 100644
--- a/src/com/android/contacts/widget/CompositeCursorAdapter.java
+++ b/src/com/android/contacts/widget/CompositeCursorAdapter.java
@@ -108,14 +108,10 @@
     }
 
     /**
-     * Removes cursors for all partitions, closing them as necessary.
+     * Removes cursors for all partitions.
      */
     public void clearPartitions() {
         for (int i = 0; i < mSize; i++) {
-            Cursor cursor = mPartitions[i].cursor;
-            if (cursor != null && !cursor.isClosed()) {
-                cursor.close();
-            }
             mPartitions[i].cursor = null;
         }
         invalidate();