Initial cut of "Join contact" functionality in the UI
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index b8940fa..2b3af38 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -19,7 +19,6 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.ListActivity;
-import android.app.SearchManager;
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -31,7 +30,6 @@
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -42,15 +40,12 @@
 import android.provider.Contacts.Intents;
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
-import android.provider.Contacts.Presence;
-import android.provider.Contacts.Intents.Insert;
 import android.provider.Contacts.Intents.UI;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.Aggregates;
 import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Postal;
-import android.provider.ContactsContract.Data;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -110,6 +105,22 @@
     private static final int SUBACTIVITY_NEW_CONTACT = 1;
     private static final int SUBACTIVITY_VIEW_CONTACT = 2;
 
+    /**
+     * The action for the join contact activity.
+     *
+     * TODO: move to {@link Contacts}.
+     */
+    public static final String JOIN_AGGREGATE =
+            "com.android.contacts.action.JOIN_AGGREGATE";
+
+    /**
+     * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
+     * <p>
+     * Type: LONG
+     */
+    public static final String EXTRA_AGGREGATE_ID =
+            "com.android.contacts.action.AGGREGATE_ID";
+
     /** Mask for picker mode */
     static final int MODE_MASK_PICKER = 0x80000000;
     /** Mask for no presence mode */
@@ -120,6 +131,8 @@
     static final int MODE_MASK_CREATE_NEW = 0x10000000;
     /** Mask for showing photos in the list */
     static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
+    /** Mask for hiding additional information e.g. primary phone number in the list */
+    static final int MODE_MASK_NO_DATA = 0x04000000;
 
     /** Unknown mode */
     static final int MODE_UNKNOWN = 0;
@@ -151,6 +164,10 @@
 //    /** Run a search query in PICK mode, but that still launches to VIEW */
 //    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;
 
     /**
@@ -278,6 +295,8 @@
      */
     private int mQueryPersonIdIndex;
 
+    private long mQueryAggregateId;
+
     /**
      * Used to keep track of the scroll state of the list.
      */
@@ -435,6 +454,17 @@
             return;
         } */
 
+        if (JOIN_AGGREGATE.equals(action)) {
+            mMode = MODE_JOIN_AGGREGATE;
+            mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
+            if (mQueryAggregateId == -1) {
+                Log.e(TAG, "Intent " + action + " is missing required extra: "
+                        + EXTRA_AGGREGATE_ID);
+                setResult(RESULT_CANCELED);
+                finish();
+            }
+        }
+
         if (mMode == MODE_UNKNOWN) {
             mMode = DEFAULT_MODE;
         }
@@ -1008,7 +1038,12 @@
                 Intent intent = new Intent(Intent.ACTION_VIEW,
                         ContentUris.withAppendedId(Aggregates.CONTENT_URI, id));
                 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
-            } /*else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
+            } else if (mMode == MODE_JOIN_AGGREGATE) {
+                Uri uri = ContentUris.withAppendedId(Aggregates.CONTENT_URI, id);
+                returnPickerResult(null, uri);
+            }
+
+            /*else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
                 // Started with query that should launch to view contact
                 Cursor c = (Cursor) mAdapter.getItem(position);
                 long personId = c.getLong(mQueryPersonIdIndex);
@@ -1202,6 +1237,14 @@
                 mQueryHandler.startQuery(QUERY_TOKEN, null, Postal.CONTENT_URI,
                         POSTALS_PROJECTION, null, null, getSortOrder(POSTALS_PROJECTION));
                 break;
+
+            case MODE_JOIN_AGGREGATE:
+                mQueryHandler.startQuery(QUERY_TOKEN, null,
+                        Aggregates.CONTENT_URI, AGGREGATES_PROJECTION,
+                        Aggregates._ID + " != " + mQueryAggregateId, null,
+                        getSortOrder(AGGREGATES_PROJECTION));
+
+                break;
         }
     }
 
@@ -1447,6 +1490,7 @@
         private CharSequence mUnknownNameText;
         private CharSequence[] mLocalizedLabels;
         private boolean mDisplayPhotos = false;
+        private boolean mDisplayAdditionalData = true;
         private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
         private boolean mDisplaySectionHeaders = true;
@@ -1475,6 +1519,18 @@
                     break;
             }
 
+            // Do not display the second line of text if in a specific SEARCH query mode, usually for
+            // matching a specific E-mail or phone number. Any contact details
+            // shown would be identical, and columns might not even be present
+            // in the returned cursor.
+            if (mQueryMode != QUERY_MODE_NONE) {
+                mDisplayAdditionalData = false;
+            }
+
+            if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
+                mDisplayAdditionalData = false;
+            }
+
             if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
                 mDisplayPhotos = true;
                 setViewResource(R.layout.contacts_list_item_photo);
@@ -1635,11 +1691,7 @@
                 cache.nameView.setText(mUnknownNameText);
             }
 
-            // Bail out early if using a specific SEARCH query mode, usually for
-            // matching a specific E-mail or phone number. Any contact details
-            // shown would be identical, and columns might not even be present
-            // in the returned cursor.
-            if (mQueryMode != QUERY_MODE_NONE) {
+            if (!mDisplayAdditionalData) {
                 cache.dataView.setVisibility(View.GONE);
                 cache.labelView.setVisibility(View.GONE);
                 cache.presenceView.setVisibility(View.GONE);
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index 451caa8..5fe9fb8 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -58,6 +58,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Aggregates;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds;
@@ -94,15 +95,13 @@
 
     private static final int DIALOG_CONFIRM_DELETE = 1;
 
+    private static final int REQUEST_JOIN_AGGREGATE = 1;
+
     public static final int MENU_ITEM_DELETE = 1;
     public static final int MENU_ITEM_MAKE_DEFAULT = 2;
     public static final int MENU_ITEM_SHOW_BARCODE = 3;
     public static final int MENU_ITEM_SPLIT_AGGREGATE = 4;
-
-    private static final String[] AGGREGATION_EXCEPTIONS_PROJECTION =
-            new String[] { AggregationExceptions._ID};
-
-    private static final int AGGREGATION_EXCEPTIONS_COL_ID = 0;
+    public static final int MENU_ITEM_JOIN_AGGREGATE = 5;
 
     private Uri mUri;
     private Uri mAggDataUri;
@@ -307,6 +306,8 @@
                 .setIcon(android.R.drawable.ic_menu_delete);
         menu.add(0, MENU_ITEM_SPLIT_AGGREGATE, 0, R.string.menu_splitAggregate)
                 .setIcon(android.R.drawable.ic_menu_share);
+        menu.add(0, MENU_ITEM_JOIN_AGGREGATE, 0, R.string.menu_joinAggregate)
+                .setIcon(android.R.drawable.ic_menu_add);
         return true;
     }
 
@@ -387,6 +388,11 @@
                 return true;
             }
 
+            case MENU_ITEM_JOIN_AGGREGATE: {
+                showJoinAggregateActivity();
+                return true;
+            }
+
             // TODO(emillar) Bring this back.
             /*case MENU_ITEM_SHOW_BARCODE:
                 if (mCursor.moveToFirst()) {
@@ -502,18 +508,55 @@
     }
 
     /**
-     * Given an ID of a constituent contact, splits it off into a separate aggregate.
+     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
      */
-    protected void splitContact(long contactToSplit) {
+    public void showJoinAggregateActivity() {
+        Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
+        intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, ContentUris.parseId(mUri));
+        startActivityForResult(intent, REQUEST_JOIN_AGGREGATE);
+    }
 
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (requestCode == REQUEST_JOIN_AGGREGATE && resultCode == RESULT_OK && intent != null) {
+            final long aggregateId = ContentUris.parseId(intent.getData());
+            joinAggregate(aggregateId);
+        }
+    }
+
+    private void splitContact(long contactId) {
+        setAggregationException(contactId, AggregationExceptions.TYPE_KEEP_OUT);
+        Toast.makeText(this, R.string.contactSplitMessage, Toast.LENGTH_SHORT);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private void joinAggregate(final long aggregateId) {
+        Cursor c = mResolver.query(Contacts.CONTENT_URI, new String[] {Contacts._ID},
+                Contacts.AGGREGATE_ID + "=" + aggregateId, null, null);
+
+        try {
+            while(c.moveToNext()) {
+                long contactId = c.getLong(0);
+                setAggregationException(contactId, AggregationExceptions.TYPE_KEEP_IN);
+            }
+        } finally {
+            c.close();
+        }
+
+        Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_SHORT);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Given a contact ID sets an aggregation exception to either join the contact with the
+     * current aggregate or split off.
+     */
+    protected void setAggregationException(long contactId, int exceptionType) {
         ContentValues values = new ContentValues(3);
         values.put(AggregationExceptions.AGGREGATE_ID, ContentUris.parseId(mUri));
-        values.put(AggregationExceptions.CONTACT_ID, contactToSplit);
-        values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_OUT);
-
+        values.put(AggregationExceptions.CONTACT_ID, contactId);
+        values.put(AggregationExceptions.TYPE, exceptionType);
         mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
-
-        mAdapter.notifyDataSetChanged();
     }
 
     private ViewEntry getViewEntryForMenuItem(MenuItem item) {