Break up view-contact-activity into smaller pieces so that they can be better recombined. Also first import of temporary mvc-framework classes that should later go into the framework.
Bug:2579760

Change-Id: I865b6194fbd28abb415e9d54622b91d719288204
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5bb147e..a7eec1d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -352,7 +352,7 @@
         </activity>
 
         <!-- Views the details of a single contact -->
-        <activity android:name="ViewContactActivity"
+        <activity android:name=".activities.ContactDetailActivity"
             android:label="@string/viewContactTitle"
             android:theme="@style/TallTitleBarTheme">
 
diff --git a/res/layout/contact_detail.xml b/res/layout/contact_detail.xml
new file mode 100644
index 0000000..541ba1c
--- /dev/null
+++ b/res/layout/contact_detail.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.contacts.views.detail.ContactDetailView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_details"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    
+    <com.android.internal.widget.ContactHeaderWidget
+        android:id="@+id/contact_header_widget"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+        
+    <ListView android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/title_bar_shadow"
+    />
+    
+    <ScrollView android:id="@android:id/empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true"
+    >
+        <TextView android:id="@+id/emptyText"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/no_contact_details"
+            android:textSize="20sp"
+            android:textColor="?android:attr/textColorSecondary"
+            android:paddingLeft="10dip"
+            android:paddingRight="10dip"
+            android:paddingTop="10dip"
+            android:lineSpacingMultiplier="0.92"
+        />
+    </ScrollView>
+            
+</com.android.contacts.views.detail.ContactDetailView>
+
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 34ee505..90a41ca 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -77,7 +77,8 @@
         }
     }
 
-    ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections, boolean separators) {
+    protected ContactEntryAdapter(Context context, ArrayList<ArrayList<E>> sections,
+            boolean separators) {
         mContext = context;
         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
         mSections = sections;
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
new file mode 100644
index 0000000..5dabdc7
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.activities;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.views.detail.ContactDetailView;
+import com.android.contacts.views.detail.ContactLoader;
+import com.android.contacts.views.detail.ContactLoader.Result;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class ContactDetailActivity extends Activity implements ContactLoader.Callbacks,
+        DialogManager.DialogShowingViewActivity {
+    private ContactDetailView mDetails;
+    private ContactLoader mLoader;
+    private Uri mUri;
+    private DialogManager mDialogManager;
+
+    private static final String TAG = "ContactDetailActivity";
+
+    private static final int DIALOG_VIEW_DIALOGS_ID1 = 1;
+    private static final int DIALOG_VIEW_DIALOGS_ID2 = 2;
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+
+        setContentView(R.layout.contact_detail);
+
+        mDialogManager = new DialogManager(this, DIALOG_VIEW_DIALOGS_ID1, DIALOG_VIEW_DIALOGS_ID2);
+
+        mDetails = (ContactDetailView) findViewById(R.id.contact_details);
+        mDetails.setCallbacks(new ContactDetailView.DefaultCallbacks(this));
+
+        mUri = getIntent().getData();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        if (mLoader == null) {
+            // Look for a passed along loader and create a new one if it's not there
+            mLoader = (ContactLoader) getLastNonConfigurationInstance();
+            if (mLoader == null) {
+                mLoader = new ContactLoader(this, mUri);
+            }
+        }
+        mLoader.registerCallbacks(this);
+        mLoader.startLoading();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        // Let the loader know we're done with it
+        mLoader.unregisterCallbacks(this);
+
+        // The loader isn't getting passed along to the next instance so ask it to stop loading
+        // TODO: Readd this once we have framework support
+        /*if (!isChangingConfigurations()) {
+            mLoader.stopLoading();
+        }*/
+    }
+
+    @Override
+    public Object onRetainNonConfigurationInstance() {
+        // Pass the loader along to the next guy
+        Object result = mLoader;
+        mLoader = null;
+        return result;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (mLoader != null) {
+            mLoader.destroy();
+        }
+    }
+
+    public void onContactLoaded(Result contact) {
+        if (contact == ContactLoader.Result.NOT_FOUND) {
+            // Item has been deleted
+            Log.i(TAG, "No contact found. Closing activity");
+            finish();
+            return;
+        }
+        mDetails.setData(contact);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mDetails.onCreateOptionsMenu(menu, getMenuInflater())) return true;
+
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // TODO: This is too hardwired.
+        if (mDetails.onPrepareOptionsMenu(menu)) return true;
+
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mDetails.onOptionsItemSelected(item)) return true;
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    public DialogManager getDialogManager() {
+        return mDialogManager;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle args) {
+        return mDialogManager.onCreateDialog(id, args);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        // TODO: This is too hardwired.
+        if (mDetails.onContextItemSelected(item)) return true;
+
+        return super.onContextItemSelected(item);
+    }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+            boolean globalSearch) {
+        if (globalSearch) {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        } else {
+            ContactsSearchManager.startSearch(this, initialQuery);
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // TODO: This is too hardwired.
+        if (mDetails.onKeyDown(keyCode, event)) return true;
+
+        return super.onKeyDown(keyCode, event);
+    }
+}
diff --git a/src/com/android/contacts/mvcframework/CursorLoader.java b/src/com/android/contacts/mvcframework/CursorLoader.java
new file mode 100644
index 0000000..89ae997
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/CursorLoader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+
+public abstract class CursorLoader extends Loader<CursorLoader.Callbacks> {
+    private Context mContext;
+    private Cursor mCursor;
+    private ForceLoadContentObserver mObserver;
+    private boolean mClosed;
+
+    public interface Callbacks {
+        public void onCursorLoaded(Cursor cursor);
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    final class LoadListTask extends AsyncTask<Void, Void, Cursor> {
+        /* Runs on a worker thread */
+        @Override
+        protected Cursor doInBackground(Void... params) {
+            Cursor cursor = doQueryInBackground();
+            // Ensure the data is loaded
+            if (cursor != null) {
+                cursor.getCount();
+                cursor.registerContentObserver(mObserver);
+            }
+            return cursor;
+        }
+
+        /* Runs on the UI thread */
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            if (mClosed) {
+                // An async query came in after the call to close()
+                cursor.close();
+                return;
+            }
+            mCursor = cursor;
+            if (mCallbacks != null) {
+                // A listener is register, notify them of the result
+                mCallbacks.onCursorLoaded(cursor);
+            }
+        }
+    }
+
+    public CursorLoader(Context context) {
+        mContext = context.getApplicationContext();
+        mObserver = new ForceLoadContentObserver();
+    }
+
+    /**
+     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+     * will be called on the UI thread. If a previous load has been completed and is still valid
+     * the result may be passed to the callbacks immediately.
+     *
+     * Must be called from the UI thread
+     */
+    @Override
+    public void startLoading() {
+        if (mCursor != null) {
+            mCallbacks.onCursorLoaded(mCursor);
+        } else {
+            forceLoad();
+        }
+    }
+
+    /**
+     * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+     * loaded data set and load a new one.
+     */
+    @Override
+    public void forceLoad() {
+        new LoadListTask().execute((Void[]) null);
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    @Override
+    public void stopLoading() {
+        if (mCursor != null && !mCursor.isClosed()) {
+            mCursor.close();
+            mCursor = null;
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // Close up the cursor
+        stopLoading();
+        // Make sure that any outstanding loads clean themselves up properly
+        mClosed = true;
+    }
+
+    /** Called from a worker thread to execute the desired query */
+    protected abstract Cursor doQueryInBackground();
+}
diff --git a/src/com/android/contacts/ui/DialogManager.java b/src/com/android/contacts/mvcframework/DialogManager.java
similarity index 88%
rename from src/com/android/contacts/ui/DialogManager.java
rename to src/com/android/contacts/mvcframework/DialogManager.java
index 469af06..420c4b2 100644
--- a/src/com/android/contacts/ui/DialogManager.java
+++ b/src/com/android/contacts/mvcframework/DialogManager.java
@@ -1,4 +1,20 @@
-package com.android.contacts.ui;
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
 
 import android.app.Activity;
 import android.app.Dialog;
diff --git a/src/com/android/contacts/mvcframework/Loader.java b/src/com/android/contacts/mvcframework/Loader.java
new file mode 100644
index 0000000..456b53a
--- /dev/null
+++ b/src/com/android/contacts/mvcframework/Loader.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.mvcframework;
+
+import android.database.ContentObserver;
+import android.os.Handler;
+
+public abstract class Loader<E> {
+    protected E mCallbacks;
+
+    protected final class ForceLoadContentObserver extends ContentObserver {
+        public ForceLoadContentObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            forceLoad();
+        }
+    }
+
+    /**
+     * Registers a class that will receive callbacks when a load is complete. The callbacks will
+     * be called on the UI thread so it's safe to pass the results to widgets.
+     *
+     * Must be called from the UI thread
+     */
+    public void registerCallbacks(E callbacks) {
+        if (mCallbacks != null) {
+            throw new IllegalStateException("There are already callbacks registered");
+        }
+        mCallbacks = callbacks;
+    }
+
+    /**
+     * Must be called from the UI thread
+     */
+    public void unregisterCallbacks(E callbacks) {
+        if (mCallbacks == null) {
+            throw new IllegalStateException("No callbacks register");
+        }
+        if (mCallbacks != callbacks) {
+            throw new IllegalArgumentException("Attempting to unregister the wrong callbacks");
+        }
+        mCallbacks = null;
+    }
+
+    /**
+     * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
+     * will be called on the UI thread. If a previous load has been completed and is still valid
+     * the result may be passed to the callbacks immediately. The loader will monitor the source of
+     * the data set and may deliver future callbacks if the source changes. Calling
+     * {@link #stopLoading} will stop the delivery of callbacks.
+     *
+     * Must be called from the UI thread
+     */
+    public abstract void startLoading();
+
+    /**
+     * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously
+     * loaded data set and load a new one.
+     */
+    public abstract void forceLoad();
+
+    /**
+     * Stops delivery of updates.
+     */
+    public abstract void stopLoading();
+
+    /**
+     * Must be called from the UI thread
+     */
+    public abstract void destroy();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 08f5ac1..993417d 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -30,6 +30,7 @@
 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.mvcframework.DialogManager;
 import com.android.contacts.ui.widget.BaseContactEditorView;
 import com.android.contacts.ui.widget.PhotoEditorView;
 import com.android.contacts.util.EmptyService;
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index cfee1c3..30ef8c1 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -25,9 +25,9 @@
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.ui.DialogManager;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.mvcframework.DialogManager.DialogShowingView;
 import com.android.contacts.ui.ViewIdGenerator;
-import com.android.contacts.ui.DialogManager.DialogShowingView;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
diff --git a/src/com/android/contacts/views/detail/ContactDetailView.java b/src/com/android/contacts/views/detail/ContactDetailView.java
new file mode 100644
index 0000000..fd68bd2
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactDetailView.java
@@ -0,0 +1,1038 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.detail;
+
+import com.android.contacts.Collapser;
+import com.android.contacts.ContactEntryAdapter;
+import com.android.contacts.ContactOptionsActivity;
+import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.TypePrecedence;
+import com.android.contacts.Collapser.Collapsible;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.mvcframework.DialogManager;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.views.detail.ContactLoader.Result;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.widget.ContactHeaderWidget;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.content.Intent;
+import android.content.Entity.NamedContentValues;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.ParseException;
+import android.net.Uri;
+import android.net.WebAddress;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnCreateContextMenuListener;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+
+public class ContactDetailView extends LinearLayout implements OnCreateContextMenuListener,
+        OnItemClickListener, DialogManager.DialogShowingView {
+    private static final String TAG = "ContactDetailsView";
+    private static final boolean SHOW_SEPARATORS = false;
+
+    private static final String DIALOG_ID_KEY = "dialog_id";
+    private static final int DIALOG_CONFIRM_DELETE = 1;
+    private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
+    private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
+    private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
+
+    private static final int MENU_ITEM_MAKE_DEFAULT = 3;
+
+    private Result mContactData;
+    private Callbacks mCallbacks;
+    private LayoutInflater mInflater;
+    private ContactHeaderWidget mContactHeaderWidget;
+    private ListView mListView;
+    private boolean mShowSmsLinksForAllPhones;
+    private ViewAdapter mAdapter;
+    private Uri mPrimaryPhoneUri = null;
+    private DialogManager mDialogManager = null;
+
+    private int mReadOnlySourcesCnt;
+    private int mWritableSourcesCnt;
+    private boolean mAllRestricted;
+    private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
+    private int mNumPhoneNumbers = 0;
+
+    /**
+     * The view shown if the detail list is empty.
+     * We set this to the list view when first bind the adapter, so that it won't be shown while
+     * we're loading data.
+     */
+    private View mEmptyView;
+
+    /**
+     * A list of distinct contact IDs included in the current contact.
+     */
+    private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
+    private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
+    private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
+
+    public ContactDetailView(Context context) {
+        super(context);
+    }
+
+    public ContactDetailView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setData(Result data) {
+        mContactData = data;
+
+        mContactHeaderWidget.bindFromContactLookupUri(data.getUri());
+        bindData();
+    }
+
+    private void bindData() {
+
+        // Build up the contact entries
+        buildEntries();
+
+        // Collapse similar data items in select sections.
+        Collapser.collapseList(mPhoneEntries);
+        Collapser.collapseList(mSmsEntries);
+        Collapser.collapseList(mEmailEntries);
+        Collapser.collapseList(mPostalEntries);
+        Collapser.collapseList(mImEntries);
+
+        if (mAdapter == null) {
+            mAdapter = new ViewAdapter(mContext, mSections);
+            mListView.setAdapter(mAdapter);
+        } else {
+            mAdapter.setSections(mSections, SHOW_SEPARATORS);
+        }
+        mListView.setEmptyView(mEmptyView);
+    }
+
+    /**
+     * Build up the entries to display on the screen.
+     */
+    private final void buildEntries() {
+        // Clear out the old entries
+        final int numSections = mSections.size();
+        for (int i = 0; i < numSections; i++) {
+            mSections.get(i).clear();
+        }
+
+        mRawContactIds.clear();
+
+        mReadOnlySourcesCnt = 0;
+        mWritableSourcesCnt = 0;
+        mAllRestricted = true;
+        mPrimaryPhoneUri = null;
+        mNumPhoneNumbers = 0;
+
+        mWritableRawContactIds.clear();
+
+        final Sources sources = Sources.getInstance(mContext);
+
+        // Build up method entries
+        if (mContactData == null) {
+            return;
+        }
+
+        for (Entity entity: mContactData.getEntities()) {
+            final ContentValues entValues = entity.getEntityValues();
+            final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
+            final long rawContactId = entValues.getAsLong(RawContacts._ID);
+
+            // Mark when this contact has any unrestricted components
+            final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
+            if (!isRestricted) mAllRestricted = false;
+
+            if (!mRawContactIds.contains(rawContactId)) {
+                mRawContactIds.add(rawContactId);
+            }
+            ContactsSource contactsSource = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
+            if (contactsSource != null && contactsSource.readOnly) {
+                mReadOnlySourcesCnt += 1;
+            } else {
+                mWritableSourcesCnt += 1;
+                mWritableRawContactIds.add(rawContactId);
+            }
+
+
+            for (NamedContentValues subValue : entity.getSubValues()) {
+                final ContentValues entryValues = subValue.values;
+                entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
+                final long dataId = entryValues.getAsLong(Data._ID);
+                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+                if (mimeType == null) continue;
+
+                final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
+                        ContactsSource.LEVEL_MIMETYPES);
+                if (kind == null) continue;
+
+                final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind,
+                        rawContactId, dataId, entryValues);
+
+                final boolean hasData = !TextUtils.isEmpty(entry.data);
+                final boolean isSuperPrimary = entryValues.getAsInteger(
+                        Data.IS_SUPER_PRIMARY) != 0;
+
+                if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build phone entries
+                    mNumPhoneNumbers++;
+
+                    entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                            Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
+                    entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
+                            Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
+
+                    // Remember super-primary phone
+                    if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
+
+                    entry.isPrimary = isSuperPrimary;
+                    mPhoneEntries.add(entry);
+
+                    if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
+                            || mShowSmsLinksForAllPhones) {
+                        // Add an SMS entry
+                        if (kind.iconAltRes > 0) {
+                            entry.secondaryActionIcon = kind.iconAltRes;
+                        }
+                    }
+                } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build email entries
+                    entry.intent = new Intent(Intent.ACTION_SENDTO,
+                            Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
+                    entry.isPrimary = isSuperPrimary;
+                    mEmailEntries.add(entry);
+
+                    // When Email rows have status, create additional Im row
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    if (status != null) {
+                        final String imMime = Im.CONTENT_ITEM_TYPE;
+                        final DataKind imKind = sources.getKindOrFallback(accountType,
+                                imMime, mContext, ContactsSource.LEVEL_MIMETYPES);
+                        final ViewEntry imEntry = ViewEntry.fromValues(mContext,
+                                imMime, imKind, rawContactId, dataId, entryValues);
+                        imEntry.intent = ContactsUtils.buildImIntent(entryValues);
+                        imEntry.applyStatus(status, false);
+                        mImEntries.add(imEntry);
+                    }
+                } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build postal entries
+                    entry.maxLines = 4;
+                    entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+                    mPostalEntries.add(entry);
+                } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build IM entries
+                    entry.intent = ContactsUtils.buildImIntent(entryValues);
+                    if (TextUtils.isEmpty(entry.label)) {
+                        entry.label = mContext.getString(R.string.chat).toLowerCase();
+                    }
+
+                    // Apply presence and status details when available
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    if (status != null) {
+                        entry.applyStatus(status, false);
+                    }
+                    mImEntries.add(entry);
+                } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
+                        (hasData || !TextUtils.isEmpty(entry.label))) {
+                    // Build organization entries
+                    final boolean isNameRawContact =
+                            (mContactData.getNameRawContactId() == rawContactId);
+
+                    final boolean duplicatesTitle =
+                            isNameRawContact
+                            && mContactData.getDisplayNameSource()
+                                == DisplayNameSources.ORGANIZATION
+                            && (!hasData || TextUtils.isEmpty(entry.label));
+
+                    if (!duplicatesTitle) {
+                        entry.uri = null;
+
+                        if (TextUtils.isEmpty(entry.label)) {
+                            entry.label = entry.data;
+                            entry.data = "";
+                        }
+
+                        mOrganizationEntries.add(entry);
+                    }
+                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build nickname entries
+                    final boolean isNameRawContact =
+                        (mContactData.getNameRawContactId() == rawContactId);
+
+                    final boolean duplicatesTitle =
+                        isNameRawContact
+                        && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+                    if (!duplicatesTitle) {
+                        entry.uri = null;
+                        mNicknameEntries.add(entry);
+                    }
+                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build note entries
+                    entry.uri = null;
+                    entry.maxLines = 100;
+                    mOtherEntries.add(entry);
+                } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                    // Build note entries
+                    entry.uri = null;
+                    entry.maxLines = 10;
+                    try {
+                        WebAddress webAddress = new WebAddress(entry.data);
+                        entry.intent = new Intent(Intent.ACTION_VIEW,
+                                Uri.parse(webAddress.toString()));
+                    } catch (ParseException e) {
+                        Log.e(TAG, "Couldn't parse website: " + entry.data);
+                    }
+                    mOtherEntries.add(entry);
+                } else {
+                    // Handle showing custom rows
+                    entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
+
+                    // Use social summary when requested by external source
+                    final DataStatus status = mContactData.getStatuses().get(entry.id);
+                    final boolean hasSocial = kind.actionBodySocial && status != null;
+                    if (hasSocial) {
+                        entry.applyStatus(status, true);
+                    }
+
+                    if (hasSocial || hasData) {
+                        mOtherEntries.add(entry);
+                    }
+                }
+            }
+        }
+    }
+
+    public interface Callbacks {
+        public void onPrimaryClick(ViewEntry entry);
+        public void onSecondaryClick(ViewEntry entry);
+    }
+
+    public static final class DefaultCallbacks implements Callbacks {
+        private Context mContext;
+
+        public DefaultCallbacks(Context context) {
+            mContext = context;
+        }
+
+        public void onPrimaryClick(ViewEntry entry) {
+            Intent intent = entry.intent;
+            if (intent != null) {
+                try {
+                    mContext.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Log.e(TAG, "No activity found for intent: " + intent);
+                }
+            }
+        }
+
+        public void onSecondaryClick(ViewEntry entry) {
+            Intent intent = entry.secondaryIntent;
+            if (intent != null) {
+                try {
+                    mContext.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Log.e(TAG, "No activity found for intent: " + intent);
+                }
+            }
+        }
+    }
+
+    public void setCallbacks(Callbacks callbacks) {
+        mCallbacks = callbacks;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        Context context = getContext();
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mContactHeaderWidget = (ContactHeaderWidget) findViewById(R.id.contact_header_widget);
+        mContactHeaderWidget.showStar(true);
+        mContactHeaderWidget.setExcludeMimes(new String[] {
+            Contacts.CONTENT_ITEM_TYPE
+        });
+
+        mListView = (ListView) findViewById(android.R.id.list);
+        mListView.setOnCreateContextMenuListener(this);
+        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+        mListView.setOnItemClickListener(this);
+        // Don't set it to mListView yet.  We do so later when we bind the adapter.
+        mEmptyView = findViewById(android.R.id.empty);
+
+        // Build the list of sections. The order they're added to mSections dictates the
+        // order they are displayed in the list.
+        mSections.add(mPhoneEntries);
+        mSections.add(mSmsEntries);
+        mSections.add(mEmailEntries);
+        mSections.add(mImEntries);
+        mSections.add(mPostalEntries);
+        mSections.add(mNicknameEntries);
+        mSections.add(mOrganizationEntries);
+        mSections.add(mGroupEntries);
+        mSections.add(mOtherEntries);
+
+        //TODO Read this value from a preference
+        mShowSmsLinksForAllPhones = true;
+    }
+
+    /* package */ static String buildActionString(DataKind kind, ContentValues values,
+            boolean lowerCase, Context context) {
+        if (kind.actionHeader == null) {
+            return null;
+        }
+        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+        if (actionHeader == null) {
+            return null;
+        }
+        return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+    }
+
+    /* package */ static String buildDataString(DataKind kind, ContentValues values,
+            Context context) {
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+    /**
+     * A basic structure with the data for a contact entry in the list.
+     */
+    static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+        public Context context = null;
+        public String resPackageName = null;
+        public int actionIcon = -1;
+        public boolean isPrimary = false;
+        public int secondaryActionIcon = -1;
+        public Intent intent;
+        public Intent secondaryIntent = null;
+        public int maxLabelLines = 1;
+        public ArrayList<Long> ids = new ArrayList<Long>();
+        public int collapseCount = 0;
+
+        public int presence = -1;
+
+        public CharSequence footerLine = null;
+
+        private ViewEntry() {
+        }
+
+        /**
+         * Build new {@link ViewEntry} and populate from the given values.
+         */
+        public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
+                long rawContactId, long dataId, ContentValues values) {
+            final ViewEntry entry = new ViewEntry();
+            entry.context = context;
+            entry.contactId = rawContactId;
+            entry.id = dataId;
+            entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
+            entry.mimetype = mimeType;
+            entry.label = buildActionString(kind, values, false, context);
+            entry.data = buildDataString(kind, values, context);
+
+            if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
+                entry.type = values.getAsInteger(kind.typeColumn);
+            }
+            if (kind.iconRes > 0) {
+                entry.resPackageName = kind.resPackageName;
+                entry.actionIcon = kind.iconRes;
+            }
+
+            return entry;
+        }
+
+        /**
+         * Apply given {@link DataStatus} values over this {@link ViewEntry}
+         *
+         * @param fillData When true, the given status replaces {@link #data}
+         *            and {@link #footerLine}. Otherwise only {@link #presence}
+         *            is updated.
+         */
+        public ViewEntry applyStatus(DataStatus status, boolean fillData) {
+            presence = status.getPresence();
+            if (fillData && status.isValid()) {
+                this.data = status.getStatus().toString();
+                this.footerLine = status.getTimestampLabel(context);
+            }
+
+            return this;
+        }
+
+        public boolean collapseWith(ViewEntry entry) {
+            // assert equal collapse keys
+            if (!shouldCollapseWith(entry)) {
+                return false;
+            }
+
+            // Choose the label associated with the highest type precedence.
+            if (TypePrecedence.getTypePrecedence(mimetype, type)
+                    > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
+                type = entry.type;
+                label = entry.label;
+            }
+
+            // Choose the max of the maxLines and maxLabelLines values.
+            maxLines = Math.max(maxLines, entry.maxLines);
+            maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
+
+            // Choose the presence with the highest precedence.
+            if (StatusUpdates.getPresencePrecedence(presence)
+                    < StatusUpdates.getPresencePrecedence(entry.presence)) {
+                presence = entry.presence;
+            }
+
+            // If any of the collapsed entries are primary make the whole thing primary.
+            isPrimary = entry.isPrimary ? true : isPrimary;
+
+            // uri, and contactdId, shouldn't make a difference. Just keep the original.
+
+            // Keep track of all the ids that have been collapsed with this one.
+            ids.add(entry.id);
+            collapseCount++;
+            return true;
+        }
+
+        public boolean shouldCollapseWith(ViewEntry entry) {
+            if (entry == null) {
+                return false;
+            }
+
+            if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
+                    entry.data)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(mimetype, entry.mimetype)
+                    || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
+                    || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
+                    || actionIcon != entry.actionIcon) {
+                return false;
+            }
+
+            return true;
+        }
+    }
+
+    /** Cache of the children views of a row */
+    private static class ViewCache {
+        public TextView label;
+        public TextView data;
+        public TextView footer;
+        public ImageView actionIcon;
+        public ImageView presenceIcon;
+        public ImageView primaryIcon;
+        public ImageView secondaryActionButton;
+        public View secondaryActionDivider;
+
+        // Need to keep track of this too
+        public ViewEntry entry;
+    }
+
+    final class ViewAdapter extends ContactEntryAdapter<ViewEntry> implements OnClickListener {
+
+        ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
+            super(context, sections, SHOW_SEPARATORS);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final ViewEntry entry = getEntry(mSections, position, false);
+            final View v;
+            final ViewCache views;
+
+            // Check to see if we can reuse convertView
+            if (convertView != null) {
+                v = convertView;
+                views = (ViewCache) v.getTag();
+            } else {
+                // Create a new view if needed
+                v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
+
+                // Cache the children
+                views = new ViewCache();
+                views.label = (TextView) v.findViewById(android.R.id.text1);
+                views.data = (TextView) v.findViewById(android.R.id.text2);
+                views.footer = (TextView) v.findViewById(R.id.footer);
+                views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
+                views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
+                views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
+                views.secondaryActionButton = (ImageView) v.findViewById(
+                        R.id.secondary_action_button);
+                views.secondaryActionButton.setOnClickListener(this);
+                views.secondaryActionDivider = v.findViewById(R.id.divider);
+                v.setTag(views);
+            }
+
+            // Update the entry in the view cache
+            views.entry = entry;
+
+            // Bind the data to the view
+            bindView(v, entry);
+            return v;
+        }
+
+        @Override
+        protected View newView(int position, ViewGroup parent) {
+            // getView() handles this
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        protected void bindView(View view, ViewEntry entry) {
+            final Resources resources = mContext.getResources();
+            ViewCache views = (ViewCache) view.getTag();
+
+            // Set the label
+            TextView label = views.label;
+            setMaxLines(label, entry.maxLabelLines);
+            label.setText(entry.label);
+
+            // Set the data
+            TextView data = views.data;
+            if (data != null) {
+                if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+                        || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
+                    data.setText(PhoneNumberUtils.formatNumber(entry.data));
+                } else {
+                    data.setText(entry.data);
+                }
+                setMaxLines(data, entry.maxLines);
+            }
+
+            // Set the footer
+            if (!TextUtils.isEmpty(entry.footerLine)) {
+                views.footer.setText(entry.footerLine);
+                views.footer.setVisibility(View.VISIBLE);
+            } else {
+                views.footer.setVisibility(View.GONE);
+            }
+
+            // Set the primary icon
+            views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
+
+            // Set the action icon
+            ImageView action = views.actionIcon;
+            if (entry.actionIcon != -1) {
+                Drawable actionIcon;
+                if (entry.resPackageName != null) {
+                    // Load external resources through PackageManager
+                    actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+                            entry.actionIcon, null);
+                } else {
+                    actionIcon = resources.getDrawable(entry.actionIcon);
+                }
+                action.setImageDrawable(actionIcon);
+                action.setVisibility(View.VISIBLE);
+            } else {
+                // Things should still line up as if there was an icon, so make it invisible
+                action.setVisibility(View.INVISIBLE);
+            }
+
+            // Set the presence icon
+            Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
+                    mContext, entry.presence);
+            ImageView presenceIconView = views.presenceIcon;
+            if (presenceIcon != null) {
+                presenceIconView.setImageDrawable(presenceIcon);
+                presenceIconView.setVisibility(View.VISIBLE);
+            } else {
+                presenceIconView.setVisibility(View.GONE);
+            }
+
+            // Set the secondary action button
+            ImageView secondaryActionView = views.secondaryActionButton;
+            Drawable secondaryActionIcon = null;
+            if (entry.secondaryActionIcon != -1) {
+                secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
+            }
+            if (entry.secondaryIntent != null && secondaryActionIcon != null) {
+                secondaryActionView.setImageDrawable(secondaryActionIcon);
+                secondaryActionView.setTag(entry.secondaryIntent);
+                secondaryActionView.setVisibility(View.VISIBLE);
+                views.secondaryActionDivider.setVisibility(View.VISIBLE);
+            } else {
+                secondaryActionView.setVisibility(View.GONE);
+                views.secondaryActionDivider.setVisibility(View.GONE);
+            }
+        }
+
+        private void setMaxLines(TextView textView, int maxLines) {
+            if (maxLines == 1) {
+                textView.setSingleLine(true);
+                textView.setEllipsize(TextUtils.TruncateAt.END);
+            } else {
+                textView.setSingleLine(false);
+                textView.setMaxLines(maxLines);
+                textView.setEllipsize(null);
+            }
+        }
+
+        public void onClick(View v) {
+            Intent intent = (Intent) v.getTag();
+            mContext.startActivity(intent);
+        }
+    }
+
+    public boolean onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
+        inflater.inflate(R.menu.view, menu);
+        return true;
+    }
+
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // Only allow edit when we have at least one raw_contact id
+        final boolean hasRawContact = (mRawContactIds.size() > 0);
+        menu.findItem(R.id.menu_edit).setEnabled(hasRawContact);
+
+        // Only allow share when unrestricted contacts available
+        menu.findItem(R.id.menu_share).setEnabled(!mAllRestricted);
+
+        return true;
+    }
+
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_edit: {
+                if (mRawContactIds.size() > 0) {
+                    long rawContactIdToEdit = mRawContactIds.get(0);
+                    Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
+                            rawContactIdToEdit);
+                    mContext.startActivity(new Intent(Intent.ACTION_EDIT, rawContactUri));
+                    return true;
+                } else {
+                    // There is no rawContact to edit.
+                    return false;
+                }
+            }
+            case R.id.menu_delete: {
+                showDeleteConfirmationDialog();
+                return true;
+            }
+            case R.id.menu_options: {
+                final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
+                intent.setData(mContactData.getLookupUri());
+                mContext.startActivity(intent);
+                return true;
+            }
+            case R.id.menu_share: {
+                if (mAllRestricted) return false;
+
+                final String lookupKey = mContactData.getLookupKey();
+                final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
+
+                final Intent intent = new Intent(Intent.ACTION_SEND);
+                intent.setType(Contacts.CONTENT_VCARD_TYPE);
+                intent.putExtra(Intent.EXTRA_STREAM, shareUri);
+
+                // Launch chooser to share contact via
+                final CharSequence chooseTitle = mContext.getText(R.string.share_via);
+                final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+                try {
+                    mContext.startActivity(chooseIntent);
+                } catch (ActivityNotFoundException ex) {
+                    Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void showDeleteConfirmationDialog() {
+        if (mReadOnlySourcesCnt > 0 & mWritableSourcesCnt > 0) {
+            showDialog(DIALOG_CONFIRM_READONLY_DELETE);
+        } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
+            showDialog(DIALOG_CONFIRM_READONLY_HIDE);
+        } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
+            showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
+        } else {
+            showDialog(DIALOG_CONFIRM_DELETE);
+        }
+    }
+
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+        } catch (ClassCastException e) {
+            Log.e(TAG, "bad menuInfo", e);
+            return;
+        }
+
+        // This can be null sometimes, don't crash...
+        if (info == null) {
+            Log.e(TAG, "bad menuInfo");
+            return;
+        }
+
+        ViewEntry entry = ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+        menu.setHeaderTitle(R.string.contactOptionsTitle);
+        if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
+            menu.add(0, 0, 0, R.string.menu_sendSMS).setIntent(entry.secondaryIntent);
+            if (!entry.isPrimary) {
+                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultNumber);
+            }
+        } else if (entry.mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_sendEmail).setIntent(entry.intent);
+            if (!entry.isPrimary) {
+                menu.add(0, MENU_ITEM_MAKE_DEFAULT, 0, R.string.menu_makeDefaultEmail);
+            }
+        } else if (entry.mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
+            menu.add(0, 0, 0, R.string.menu_viewAddress).setIntent(entry.intent);
+        }
+    }
+
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
+        if (entry == null) {
+            signalError();
+            return;
+        }
+        Intent intent = entry.intent;
+        if (intent == null) {
+            signalError();
+            return;
+        }
+        try {
+            mContext.startActivity(intent);
+        } catch (ActivityNotFoundException e) {
+            Log.e(TAG, "No activity found for intent: " + intent);
+            signalError();
+        }
+    }
+
+    /**
+     * Signal an error to the user via a beep, or some other method.
+     */
+    private void signalError() {
+        Log.w(TAG, "Should warn the user but we can't because we do not have sonification APIs");
+    }
+
+    private final DialogInterface.OnClickListener mDeleteListener =
+            new DialogInterface.OnClickListener() {
+        public void onClick(DialogInterface dialog, int which) {
+            mContext.getContentResolver().delete(mContactData.getLookupUri(), null, null);
+        }
+    };
+
+    public Dialog createDialog(Bundle bundle) {
+        if (bundle == null) throw new IllegalArgumentException("bundle must not be null");
+        int dialogId = bundle.getInt(DIALOG_ID_KEY);
+        switch (dialogId) {
+            case DIALOG_CONFIRM_DELETE:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.deleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case DIALOG_CONFIRM_READONLY_DELETE:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case DIALOG_CONFIRM_MULTIPLE_DELETE:
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.multipleContactDeleteConfirmation)
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .setCancelable(false)
+                        .create();
+            case DIALOG_CONFIRM_READONLY_HIDE: {
+                return new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.deleteConfirmation_title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(R.string.readOnlyContactWarning)
+                        .setPositiveButton(android.R.string.ok, mDeleteListener)
+                        .create();
+            }
+            default:
+                throw new IllegalArgumentException("Invalid dialogId: " + dialogId);
+        }
+    }
+
+    /* package */ void showDialog(int bundleDialogId) {
+        Bundle bundle = new Bundle();
+        bundle.putInt(DIALOG_ID_KEY, bundleDialogId);
+        getDialogManager().showDialogInView(this, bundle);
+    }
+
+    private DialogManager getDialogManager() {
+        if (mDialogManager == null) {
+            Context context = getContext();
+            if (!(context instanceof DialogManager.DialogShowingViewActivity)) {
+                throw new IllegalStateException(
+                        "View must be hosted in an Activity that implements " +
+                        "DialogManager.DialogShowingViewActivity");
+            }
+            mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager();
+        }
+        return mDialogManager;
+    }
+
+    public boolean onContextItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case MENU_ITEM_MAKE_DEFAULT: {
+                if (makeItemDefault(item)) {
+                    return true;
+                }
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean makeItemDefault(MenuItem item) {
+        ViewEntry entry = getViewEntryForMenuItem(item);
+        if (entry == null) {
+            return false;
+        }
+
+        // Update the primary values in the data record.
+        ContentValues values = new ContentValues(1);
+        values.put(Data.IS_SUPER_PRIMARY, 1);
+
+        mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
+                values, null, null);
+        return true;
+    }
+
+    private ViewEntry getViewEntryForMenuItem(MenuItem item) {
+        AdapterView.AdapterContextMenuInfo info;
+        try {
+             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+        } catch (ClassCastException e) {
+            Log.e(TAG, "bad menuInfo", e);
+            return null;
+        }
+
+        return ContactEntryAdapter.getEntry(mSections, info.position, SHOW_SEPARATORS);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CALL: {
+                try {
+                    ITelephony phone = ITelephony.Stub.asInterface(
+                            ServiceManager.checkService("phone"));
+                    if (phone != null && !phone.isIdle()) {
+                        // Skip out and let the key be handled at a higher level
+                        break;
+                    }
+                } catch (RemoteException re) {
+                    // Fall through and try to call the contact
+                }
+
+                int index = mListView.getSelectedItemPosition();
+                if (index != -1) {
+                    final ViewEntry entry = ViewAdapter.getEntry(mSections, index, SHOW_SEPARATORS);
+                    if (entry != null &&
+                            entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) {
+                        mContext.startActivity(entry.intent);
+                        return true;
+                    }
+                } else if (mPrimaryPhoneUri != null) {
+                    // There isn't anything selected, call the default number
+                    final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+                            mPrimaryPhoneUri);
+                    mContext.startActivity(intent);
+                    return true;
+                }
+                return false;
+            }
+
+            case KeyEvent.KEYCODE_DEL: {
+                showDeleteConfirmationDialog();
+                return true;
+            }
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+}
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
new file mode 100644
index 0000000..38e7a5d
--- /dev/null
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.detail;
+
+import com.android.contacts.mvcframework.Loader;
+import com.android.contacts.util.DataStatus;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.StatusUpdates;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactLoader extends Loader<ContactLoader.Callbacks> {
+    private Context mContext;
+    private Uri mLookupUri;
+    private Result mContact;
+    private ForceLoadContentObserver mObserver;
+    private boolean mDestroyed;
+
+    private static final String TAG = "ContactLoader";
+
+    public interface Callbacks {
+        public void onContactLoaded(Result contact);
+    }
+
+    /**
+     * The result of a load operation. Contains all data necessary to display the contact.
+     */
+    public static final class Result {
+        /**
+         * Singleton instance that represents "No Contact Found"
+         */
+        public static final Result NOT_FOUND = new Result();
+
+        private final Uri mLookupUri;
+        private final String mLookupKey;
+        private final Uri mUri;
+        private final long mId;
+        private final ArrayList<Entity> mEntities;
+        private final HashMap<Long, DataStatus> mStatuses;
+        private final long mNameRawContactId;
+        private final int mDisplayNameSource;
+
+        /**
+         * Constructor for case "no contact found". This must only be used for the
+         * final {@link Result#NOT_FOUND} singleton
+         */
+        private Result() {
+            mLookupUri = null;
+            mLookupKey = null;
+            mUri = null;
+            mId = -1;
+            mEntities = null;
+            mStatuses = null;
+            mNameRawContactId = -1;
+            mDisplayNameSource = DisplayNameSources.UNDEFINED;
+        }
+
+        /**
+         * Constructor to call when contact was found
+         */
+        private Result(Uri lookupUri, String lookupKey, Uri uri, long id, long nameRawContactId,
+                int displayNameSource) {
+            mLookupUri = lookupUri;
+            mLookupKey = lookupKey;
+            mUri = uri;
+            mId = id;
+            mEntities = new ArrayList<Entity>();
+            mStatuses = new HashMap<Long, DataStatus>();
+            mNameRawContactId = nameRawContactId;
+            mDisplayNameSource = displayNameSource;
+        }
+
+        public Uri getLookupUri() {
+            return mLookupUri;
+        }
+        public String getLookupKey() {
+            return mLookupKey;
+        }
+        public Uri getUri() {
+            return mUri;
+        }
+        public long getId() {
+            return mId;
+        }
+        public ArrayList<Entity> getEntities() {
+            return mEntities;
+        }
+        public HashMap<Long, DataStatus> getStatuses() {
+            return mStatuses;
+        }
+        public long getNameRawContactId() {
+            return mNameRawContactId;
+        }
+        public int getDisplayNameSource() {
+            return mDisplayNameSource;
+        }
+    }
+
+    interface StatusQuery {
+        final String[] PROJECTION = new String[] {
+                Data._ID, Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+                Data.STATUS_LABEL, Data.STATUS_TIMESTAMP, Data.PRESENCE,
+        };
+
+        final int _ID = 0;
+    }
+
+    final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+        @Override
+        protected Result doInBackground(Void... args) {
+            final ContentResolver resolver = mContext.getContentResolver();
+            Result result = loadContactHeaderData(resolver, mLookupUri);
+            if (result == Result.NOT_FOUND) {
+                // No record found. Try to lookup up a new record with the same lookupKey.
+                // We might have went through a sync where Ids changed
+                final Uri freshLookupUri = Contacts.getLookupUri(resolver, mLookupUri);
+                result = loadContactHeaderData(resolver, freshLookupUri);
+                if (result == Result.NOT_FOUND) {
+                    // Still not found. We now believe this contact really does not exist
+                    Log.e(TAG, "invalid contact uri: " + mLookupUri);
+                    return Result.NOT_FOUND;
+                }
+            }
+
+            // These queries could be run in parallel (we did this until froyo). But unless
+            // we actually have two database connections there is no performance gain
+            loadSocial(resolver, result);
+            loadRawContacts(resolver, result);
+
+            return result;
+        }
+
+        /**
+         * Tries to lookup a contact using both Id and lookup key of the given Uri. Returns a
+         * valid Result instance if successful or {@link Result#NOT_FOUND} if empty
+         */
+        private Result loadContactHeaderData(final ContentResolver resolver,
+                final Uri lookupUri) {
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (lookupUri == null) {
+                // This can happen if the row was removed
+                return Result.NOT_FOUND;
+            }
+
+            final List<String> segments = lookupUri.getPathSegments();
+            if (segments.size() != 4) {
+                // Does not contain an Id. Return to caller so that a lookup is performed
+                Log.w(TAG, "Uri does not contain an Id, so we return to the caller who should " +
+                		"perform a lookup to get a proper uri. Value: " + lookupUri);
+                return Result.NOT_FOUND;
+            }
+
+            final long uriContactId = Long.parseLong(segments.get(3));
+            final String uriLookupKey = Uri.encode(segments.get(2));
+            final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, uriContactId);
+            final Uri dataUri = Uri.withAppendedPath(contactUri, Contacts.Data.CONTENT_DIRECTORY);
+
+            final Cursor cursor = resolver.query(dataUri,
+                    new String[] {
+                        Contacts.NAME_RAW_CONTACT_ID,
+                        Contacts.DISPLAY_NAME_SOURCE,
+                        Contacts.LOOKUP_KEY
+                    }, null, null, null);
+            if (cursor == null) {
+                Log.e(TAG, "No cursor returned in trySetupContactHeader/query");
+                return null;
+            }
+            try {
+                if (!cursor.moveToFirst()) {
+                    Log.w(TAG, "Cursor returned by trySetupContactHeader/query is empty. " +
+                    		"ContactId must have changed or item has been removed");
+                    return Result.NOT_FOUND;
+                }
+                String lookupKey =
+                        cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+                if (!lookupKey.equals(uriLookupKey)) {
+                    // ID and lookup key do not match
+                    Log.w(TAG, "Contact with Id=" + uriContactId + " has a wrong lookupKey ("
+                            + lookupKey + " instead of the expected " + uriLookupKey + ")");
+                    return Result.NOT_FOUND;
+                }
+
+                long nameRawContactId = cursor.getLong(cursor.getColumnIndex(
+                        Contacts.NAME_RAW_CONTACT_ID));
+                int displayNameSource = cursor.getInt(cursor.getColumnIndex(
+                        Contacts.DISPLAY_NAME_SOURCE));
+
+                return new Result(lookupUri, lookupKey, contactUri, uriContactId, nameRawContactId,
+                        displayNameSource);
+            } finally {
+                cursor.close();
+            }
+        }
+
+        /**
+         * Loads the social rows into the result structure. Expects the statuses in the
+         * result structure to be empty
+         */
+        private void loadSocial(final ContentResolver resolver, final Result result) {
+            if (result == null) throw new IllegalArgumentException("result must not be null");
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (result == Result.NOT_FOUND) {
+                throw new IllegalArgumentException("result must not be NOT_FOUND");
+            }
+
+            final Uri dataUri = Uri.withAppendedPath(result.getUri(),
+                    Contacts.Data.CONTENT_DIRECTORY);
+            final Cursor cursor = resolver.query(dataUri, StatusQuery.PROJECTION,
+                    StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS +
+                    " IS NOT NULL", null, null);
+
+            if (cursor == null) {
+                Log.e(TAG, "Social cursor is null but it shouldn't be");
+                return;
+            }
+
+            try {
+                HashMap<Long, DataStatus> statuses = result.getStatuses();
+
+                // Walk found statuses, creating internal row for each
+                while (cursor.moveToNext()) {
+                    final DataStatus status = new DataStatus(cursor);
+                    final long dataId = cursor.getLong(StatusQuery._ID);
+                    statuses.put(dataId, status);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        /**
+         * Loads the raw row contact rows into the result structure. Expects the entities in the
+         * result structure to be empty
+         */
+        private void loadRawContacts(final ContentResolver resolver, final Result result) {
+            if (result == null) throw new IllegalArgumentException("result must not be null");
+            if (resolver == null) throw new IllegalArgumentException("resolver must not be null");
+            if (result == Result.NOT_FOUND) {
+                throw new IllegalArgumentException("result must not be NOT_FOUND");
+            }
+
+            // Read the constituent raw contacts
+            final Cursor cursor = resolver.query(RawContactsEntity.CONTENT_URI, null,
+                    RawContacts.CONTACT_ID + "=?", new String[] {
+                            String.valueOf(result.mId)
+                    }, null);
+            if (cursor == null) {
+                Log.e(TAG, "Raw contacts cursor is null but it shouldn't be");
+                return;
+            }
+
+            try {
+                ArrayList<Entity> entities = result.getEntities();
+                entities.ensureCapacity(cursor.getCount());
+                EntityIterator iterator = RawContacts.newEntityIterator(cursor);
+                try {
+                    while (iterator.hasNext()) {
+                        Entity entity = iterator.next();
+                        entities.add(entity);
+                    }
+                } finally {
+                    iterator.close();
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Result result) {
+            // The creator isn't interested in any furether updates
+            if (mDestroyed) {
+                return;
+            }
+
+            mContact = result;
+            if (result != null) {
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+                mContext.getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+                mCallbacks.onContactLoaded(result);
+            }
+        }
+    }
+
+    public ContactLoader(Context context, Uri lookupUri) {
+        mContext = context.getApplicationContext();
+        mLookupUri = lookupUri;
+    }
+
+    @Override
+    public void startLoading() {
+        if (mContact != null) {
+            mCallbacks.onContactLoaded(mContact);
+        } else {
+            forceLoad();
+        }
+    }
+
+    @Override
+    public void forceLoad() {
+        new LoadContactTask().execute((Void[])null);
+    }
+
+    @Override
+    public void stopLoading() {
+        mContact = null;
+        if (mObserver != null) {
+            mContext.getContentResolver().unregisterContentObserver(mObserver);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mContact = null;
+        mDestroyed = true;
+    }
+}