Contact card with and without social updates

- This is for the phone (landscape and portrait)
- Some tweaks were done to the tablet to prevent regression
but it's not quite ready yet
- No social updates means a single scrolling list of details
- Having social updates means a tab carousel and ViewPager
- Add invisible contact loader fragment
- Now the loader fragment loads the contact --> passes to
ContactDetailActivity --> passes to all necessary fragments /
carousels (no matter the configuration)
- Get rid of ContactDetailAboutFragment and move those changes
into the ContactDetailFragment

Change-Id: I7be55ae7205bbcb8106bf2f2e4ae8dd6ce2c6a78
diff --git a/res/layout-sw580dp/simple_contact_detail_header_view_list_item.xml b/res/layout-sw580dp/simple_contact_detail_header_view_list_item.xml
new file mode 100644
index 0000000..b14d563
--- /dev/null
+++ b/res/layout-sw580dp/simple_contact_detail_header_view_list_item.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!--
+  This view temporarily holds the extra information that used to be in the
+  original contact detail header view, but now must move into the list because
+  of the new tab carousel. TODO: Integrate this better into the list as provided
+  by the mocks.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingTop="20dip">
+
+    <ImageView
+        android:id="@+id/photo"
+        android:scaleType="centerCrop"
+        android:layout_width="@dimen/detail_contact_photo_size"
+        android:layout_height="@dimen/detail_contact_photo_size" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textSize="36sp" />
+
+    <TextView
+        android:id="@+id/company"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <TextView
+        android:id="@+id/phonetic_name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall" />
+
+    <TextView
+        android:id="@+id/attribution"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall" />
+
+    <CheckBox
+        android:id="@+id/star"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top"
+        android:contentDescription="@string/description_star"
+        style="?android:attr/starStyle" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-w470dp/contact_detail_activity.xml b/res/layout-w470dp/contact_detail_container_with_updates.xml
similarity index 84%
rename from res/layout-w470dp/contact_detail_activity.xml
rename to res/layout-w470dp/contact_detail_container_with_updates.xml
index bf649a2..a8ee0a5 100644
--- a/res/layout-w470dp/contact_detail_activity.xml
+++ b/res/layout-w470dp/contact_detail_container_with_updates.xml
@@ -15,15 +15,13 @@
 -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_detail_view"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:orientation="vertical">
 
     <com.android.contacts.detail.ContactDetailFragmentCarousel
         android:id="@+id/fragment_carousel"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentLeft="true"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-</FrameLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/contact_detail_activity.xml b/res/layout/contact_detail_activity.xml
index d840d6f..3ab40c3 100644
--- a/res/layout/contact_detail_activity.xml
+++ b/res/layout/contact_detail_activity.xml
@@ -14,24 +14,17 @@
      limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/contact_detail_view"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="match_parent">
 
-    <android.support.v4.view.ViewPager
-        android:id="@+id/pager"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentLeft="true"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+    <!-- This fragment is an invisible worker fragment that loads the contact details. -->
+    <fragment
+        android:id="@+id/loader_fragment"
+        class="com.android.contacts.detail.ContactLoaderFragment"
+        android:layout_height="0dip"
+        android:layout_width="0dip"
+        android:visibility="gone"/>
 
-    <com.android.contacts.detail.ContactDetailTabCarousel
-        android:id="@+id/tab_carousel"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentLeft="true"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-
-</RelativeLayout>
+</FrameLayout>
diff --git a/res/layout/contact_detail_container_with_updates.xml b/res/layout/contact_detail_container_with_updates.xml
new file mode 100644
index 0000000..de7d145
--- /dev/null
+++ b/res/layout/contact_detail_container_with_updates.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/pager"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.android.contacts.detail.ContactDetailTabCarousel
+        android:id="@+id/tab_carousel"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+</RelativeLayout>
diff --git a/res/layout-w470dp/contact_detail_activity.xml b/res/layout/contact_detail_container_without_updates.xml
similarity index 72%
copy from res/layout-w470dp/contact_detail_activity.xml
copy to res/layout/contact_detail_container_without_updates.xml
index bf649a2..884f280 100644
--- a/res/layout-w470dp/contact_detail_activity.xml
+++ b/res/layout/contact_detail_container_without_updates.xml
@@ -14,15 +14,14 @@
      limitations under the License.
 -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_detail_view"
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <com.android.contacts.detail.ContactDetailFragmentCarousel
-        android:id="@+id/fragment_carousel"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentLeft="true"
+    <fragment
+        android:id="@+id/contact_detail_fragment"
+        class="com.android.contacts.detail.ContactDetailFragment"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
diff --git a/res/layout/contact_detail_fragment.xml b/res/layout/contact_detail_fragment.xml
index 70a9a28..d16771c 100644
--- a/res/layout/contact_detail_fragment.xml
+++ b/res/layout/contact_detail_fragment.xml
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_detail"
+    android:id="@+id/contact_detail_about_fragment"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
@@ -25,15 +25,14 @@
         android:layout_height="0px"
         android:layout_weight="1"
         android:background="@color/background_primary"
-        android:divider="@null"
-    />
+        android:divider="@null"/>
 
     <ScrollView android:id="@android:id/empty"
         android:layout_width="match_parent"
         android:layout_height="0px"
         android:layout_weight="1"
-        android:visibility="gone"
-    >
+        android:visibility="gone">
+
         <TextView android:id="@+id/emptyText"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
@@ -43,8 +42,8 @@
             android:paddingLeft="10dip"
             android:paddingRight="10dip"
             android:paddingTop="10dip"
-            android:lineSpacingMultiplier="0.92"
-        />
+            android:lineSpacingMultiplier="0.92"/>
+
     </ScrollView>
 
     <!-- "Copy to my contacts"- button -->
diff --git a/res/layout/contact_detail_fragment_carousel.xml b/res/layout/contact_detail_fragment_carousel.xml
index 165b6a1..312fdf2 100644
--- a/res/layout/contact_detail_fragment_carousel.xml
+++ b/res/layout/contact_detail_fragment_carousel.xml
@@ -21,7 +21,7 @@
     android:scrollbars="none"
     android:orientation="horizontal">
 
-    <fragment class="com.android.contacts.detail.ContactDetailAboutFragment"
+    <fragment class="com.android.contacts.detail.ContactDetailFragment"
         android:id="@+id/about_fragment"
         android:layout_width="@dimen/detail_fragment_carousel_fragment_width"
         android:layout_height="match_parent" />
diff --git a/res/layout-w470dp/contact_detail_activity.xml b/res/layout/contact_detail_loader_fragment.xml
similarity index 62%
copy from res/layout-w470dp/contact_detail_activity.xml
copy to res/layout/contact_detail_loader_fragment.xml
index bf649a2..3c4bbef 100644
--- a/res/layout-w470dp/contact_detail_activity.xml
+++ b/res/layout/contact_detail_loader_fragment.xml
@@ -15,15 +15,6 @@
 -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_detail_view"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <com.android.contacts.detail.ContactDetailFragmentCarousel
-        android:id="@+id/fragment_carousel"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentLeft="true"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-
-</FrameLayout>
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:visibility="gone"/>
\ No newline at end of file
diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml
index daa5608..e9c36f3 100644
--- a/res/layout/contact_detail_updates_fragment.xml
+++ b/res/layout/contact_detail_updates_fragment.xml
@@ -15,7 +15,7 @@
 -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/contact_detail"
+    android:id="@+id/contact_detail_updates_fragment"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
diff --git a/res/layout/favorites_star.xml b/res/layout/favorites_star.xml
index f2afa31..4b859b4 100644
--- a/res/layout/favorites_star.xml
+++ b/res/layout/favorites_star.xml
@@ -20,12 +20,15 @@
     android:layout_height="wrap_content"
     android:paddingLeft="10dip"
     android:paddingRight="10dip">
+
     <CheckBox
         android:id="@+id/star"
+        android:duplicateParentState="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:contentDescription="@string/description_star"
         android:visibility="invisible"
         style="?android:attr/starStyle"/>
+
 </FrameLayout>
\ No newline at end of file
diff --git a/res/layout/simple_contact_detail_header_view_list_item.xml b/res/layout/simple_contact_detail_header_view_list_item.xml
index 1fd9ec5..0c0867f 100644
--- a/res/layout/simple_contact_detail_header_view_list_item.xml
+++ b/res/layout/simple_contact_detail_header_view_list_item.xml
@@ -22,10 +22,15 @@
 -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:paddingTop="@dimen/detail_tab_carousel_height">
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/photo"
+        android:scaleType="centerCrop"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/detail_tab_carousel_height" />
 
     <TextView
         android:id="@+id/phonetic_name"
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index b2f2af1..f1e5736 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -32,4 +32,5 @@
     <dimen name="shortcut_icon_size">64dip</dimen>
     <dimen name="list_section_height">37dip</dimen>
     <dimen name="directory_header_height">56dip</dimen>
+    <dimen name="detail_tab_carousel_height">150dip</dimen>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 95bbb63..d248f5b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -109,6 +109,9 @@
     <!-- Margin around the contact's photo on the contact card -->
     <dimen name="detail_contact_photo_margin">15dip</dimen>
 
+    <!-- Width and height of the contact photo on the contact detail page -->
+    <dimen name="detail_contact_photo_size">256dip</dimen>
+
     <!-- Left and right padding for a contact detail item -->
     <dimen name="detail_item_icon_margin">10dip</dimen>
 
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index e216d54..d5d0255 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -21,23 +21,29 @@
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.R;
-import com.android.contacts.detail.ContactDetailAboutFragment;
 import com.android.contacts.detail.ContactDetailDisplayUtils;
 import com.android.contacts.detail.ContactDetailFragment;
 import com.android.contacts.detail.ContactDetailFragmentCarousel;
 import com.android.contacts.detail.ContactDetailTabCarousel;
 import com.android.contacts.detail.ContactDetailUpdatesFragment;
+import com.android.contacts.detail.ContactLoaderFragment;
+import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener;
 import com.android.contacts.interactions.ContactDeletionInteraction;
 import com.android.contacts.util.PhoneCapabilityTester;
 
 import android.accounts.Account;
+import android.app.ActionBar;
+import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentManager;
+import android.app.FragmentTransaction;
 import android.content.ActivityNotFoundException;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.support.v13.app.FragmentPagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.support.v4.view.ViewPager.OnPageChangeListener;
@@ -46,8 +52,9 @@
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
@@ -59,12 +66,16 @@
 public class ContactDetailActivity extends ContactsActivity {
     private static final String TAG = "ContactDetailActivity";
 
+    private static final String KEY_DETAIL_FRAGMENT_TAG = "detailFragTag";
+    private static final String KEY_UPDATES_FRAGMENT_TAG = "updatesFragTag";
+
     public static final int FRAGMENT_COUNT = 2;
 
     private ContactLoader.Result mContactData;
     private Uri mLookupUri;
 
-    private ContactDetailAboutFragment mAboutFragment;
+    private ContactLoaderFragment mLoaderFragment;
+    private ContactDetailFragment mDetailFragment;
     private ContactDetailUpdatesFragment mUpdatesFragment;
 
     private ContactDetailTabCarousel mTabCarousel;
@@ -72,6 +83,18 @@
 
     private ContactDetailFragmentCarousel mFragmentCarousel;
 
+    private ViewGroup mRootView;
+    private ViewGroup mContentView;
+    private LayoutInflater mInflater;
+
+    private Handler mHandler = new Handler();
+
+    /**
+     * Whether or not the contact has updates, which dictates whether the
+     * {@link ContactDetailUpdatesFragment} will be shown.
+     */
+    private boolean mContactHasUpdates;
+
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
@@ -93,24 +116,29 @@
         }
 
         setContentView(R.layout.contact_detail_activity);
+        mRootView = (ViewGroup) findViewById(R.id.contact_detail_view);
+        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
-        // Narrow width screens have a {@link ViewPager} and {@link ContactDetailTabCarousel}
-        mViewPager = (ViewPager) findViewById(R.id.pager);
-        if (mViewPager != null) {
-            mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
-            mViewPager.setOnPageChangeListener(mOnPageChangeListener);
-        }
+        // Manually remove any {@link ViewPager} fragments if there was an orientation change
+        // because the {@link ViewPager} is not used in both orientations. (If we leave the
+        // fragments around, they'll be around in the {@link FragmentManager} but won't be visible
+        // on screen and the {@link ViewPager} won't ask to initialize them again).
+        if (savedState != null) {
+            String aboutFragmentTag = savedState.getString(KEY_DETAIL_FRAGMENT_TAG);
+            String updatesFragmentTag = savedState.getString(KEY_UPDATES_FRAGMENT_TAG);
 
-        mTabCarousel = (ContactDetailTabCarousel) findViewById(R.id.tab_carousel);
-        if (mTabCarousel != null) {
-            mTabCarousel.setListener(mTabCarouselListener);
-        }
+            FragmentManager fragmentManager = getFragmentManager();
+            mDetailFragment = (ContactDetailFragment) fragmentManager.findFragmentByTag(
+                    aboutFragmentTag);
+            mUpdatesFragment = (ContactDetailUpdatesFragment) fragmentManager.findFragmentByTag(
+                    updatesFragmentTag);
 
-        // Otherwise, wide width screens have a {@link ContactDetailFragmentCarousel}
-        mFragmentCarousel = (ContactDetailFragmentCarousel) findViewById(R.id.fragment_carousel);
-        if (mFragmentCarousel != null) {
-            if (mAboutFragment != null) mFragmentCarousel.setAboutFragment(mAboutFragment);
-            if (mUpdatesFragment != null) mFragmentCarousel.setUpdatesFragment(mUpdatesFragment);
+            if (mDetailFragment != null && mUpdatesFragment != null) {
+                FragmentTransaction ft = fragmentManager.beginTransaction();
+                ft.remove(mDetailFragment);
+                ft.remove(mUpdatesFragment);
+                ft.commit();
+            }
         }
 
         Log.i(TAG, getIntent().getData().toString());
@@ -118,13 +146,23 @@
 
     @Override
     public void onAttachFragment(Fragment fragment) {
-        if (fragment instanceof ContactDetailAboutFragment) {
-            mAboutFragment = (ContactDetailAboutFragment) fragment;
-            mAboutFragment.setListener(mFragmentListener);
-            mAboutFragment.setVerticalScrollListener(mVerticalScrollListener);
-            mAboutFragment.loadUri(getIntent().getData());
+        if (fragment instanceof ContactDetailFragment) {
+            mDetailFragment = (ContactDetailFragment) fragment;
+            mDetailFragment.setListener(mFragmentListener);
+            mDetailFragment.setVerticalScrollListener(mVerticalScrollListener);
+            mDetailFragment.setData(mContactData);
+            // If the contact has social updates, then the photo should be shown in the tab
+            // carousel, so don't show the photo again in the scrolling list of contact details.
+            // We also don't want to show the photo if there is a fragment carousel because then
+            // the picture will already be on the left of the list of contact details.
+            mDetailFragment.setShowPhotoInHeader(!mContactHasUpdates && mFragmentCarousel == null);
         } else if (fragment instanceof ContactDetailUpdatesFragment) {
             mUpdatesFragment = (ContactDetailUpdatesFragment) fragment;
+            mUpdatesFragment.setData(mContactData);
+        } else if (fragment instanceof ContactLoaderFragment) {
+            mLoaderFragment = (ContactLoaderFragment) fragment;
+            mLoaderFragment.setListener(mLoaderFragmentListener);
+            mLoaderFragment.loadUri(getIntent().getData());
         }
     }
 
@@ -177,7 +215,7 @@
         FragmentKeyListener mCurrentFragment;
         switch (getCurrentPage()) {
             case 0:
-                mCurrentFragment = (FragmentKeyListener) mAboutFragment;
+                mCurrentFragment = (FragmentKeyListener) mDetailFragment;
                 break;
             case 1:
                 mCurrentFragment = (FragmentKeyListener) mUpdatesFragment;
@@ -191,13 +229,105 @@
     }
 
     private int getCurrentPage() {
+        // If the contact doesn't have any social updates, there is only 1 page (detail fragment).
+        if (!mContactHasUpdates) {
+            return 0;
+        }
+        // Otherwise find the current page based on the {@link ViewPager} or fragment carousel.
         if (mViewPager != null) {
             return mViewPager.getCurrentItem();
         } else if (mFragmentCarousel != null) {
             return mFragmentCarousel.getCurrentPage();
         }
-        throw new IllegalStateException("Can't figure out the currently selected page. The activity"
-                + "must either have the ViewPager or fragment carousel");
+        throw new IllegalStateException("Can't figure out the currently selected page. If the " +
+                "contact has social updates, there must be a ViewPager or fragment carousel");
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mViewPager != null) {
+            outState.putString(KEY_DETAIL_FRAGMENT_TAG, mDetailFragment.getTag());
+            outState.putString(KEY_UPDATES_FRAGMENT_TAG, mUpdatesFragment.getTag());
+            return;
+        }
+    }
+
+    private final ContactLoaderFragmentListener mLoaderFragmentListener =
+            new ContactLoaderFragmentListener() {
+        @Override
+        public void onDetailsLoaded(final ContactLoader.Result result) {
+            if (result == null) {
+                return;
+            }
+            // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the
+            // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler}
+            // on the main thread to execute later.
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mContactData = result;
+                    mLookupUri = result.getLookupUri();
+                    mContactHasUpdates = result.getSocialSnippet() != null;
+                    invalidateOptionsMenu();
+                    setupTitle();
+                    if (mContactHasUpdates) {
+                        setupContactWithUpdates();
+                    } else {
+                        setupContactWithoutUpdates();
+                    }
+                }
+            });
+        }
+    };
+
+    /**
+     * Setup the activity title and subtitle with contact name and company.
+     */
+    private void setupTitle() {
+        CharSequence displayName = ContactDetailDisplayUtils.getDisplayName(this, mContactData);
+        String company =  ContactDetailDisplayUtils.getCompany(this, mContactData);
+
+        ActionBar actionBar = getActionBar();
+        actionBar.setTitle(displayName);
+        actionBar.setSubtitle(company);
+    }
+
+    private void setupContactWithUpdates() {
+        if (mContentView == null) {
+            mContentView = (ViewGroup) mInflater.inflate(
+                    R.layout.contact_detail_container_with_updates, mRootView, false);
+            mRootView.addView(mContentView);
+        }
+
+        // Narrow width screens have a {@link ViewPager} and {@link ContactDetailTabCarousel}
+        mViewPager = (ViewPager) findViewById(R.id.pager);
+        if (mViewPager != null) {
+            mViewPager.removeAllViews();
+            mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
+            mViewPager.setOnPageChangeListener(mOnPageChangeListener);
+        }
+
+        mTabCarousel = (ContactDetailTabCarousel) findViewById(R.id.tab_carousel);
+        if (mTabCarousel != null) {
+            mTabCarousel.setListener(mTabCarouselListener);
+            mTabCarousel.loadData(mContactData);
+        }
+
+        // Otherwise, wide width screens have a {@link ContactDetailFragmentCarousel}
+        mFragmentCarousel = (ContactDetailFragmentCarousel) findViewById(R.id.fragment_carousel);
+        if (mFragmentCarousel != null) {
+            if (mDetailFragment != null) mFragmentCarousel.setAboutFragment(mDetailFragment);
+            if (mUpdatesFragment != null) mFragmentCarousel.setUpdatesFragment(mUpdatesFragment);
+        }
+    }
+
+    private void setupContactWithoutUpdates() {
+        if (mContentView == null) {
+            mContentView = (ViewGroup) mInflater.inflate(
+                    R.layout.contact_detail_container_without_updates, mRootView, false);
+            mRootView.addView(mContentView);
+        }
     }
 
     private final ContactDetailFragment.Listener mFragmentListener =
@@ -208,19 +338,6 @@
         }
 
         @Override
-        public void onDetailsLoaded(ContactLoader.Result result) {
-            if (result == null) {
-                return;
-            }
-            mContactData = result;
-            mLookupUri = result.getLookupUri();
-            invalidateOptionsMenu();
-            if (mTabCarousel != null) {
-                mTabCarousel.loadData(result);
-            }
-        }
-
-        @Override
         public void onEditRequested(Uri contactLookupUri) {
             startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri));
         }
@@ -262,7 +379,7 @@
         public Fragment getItem(int position) {
             switch (position) {
                 case 0:
-                    return new ContactDetailAboutFragment();
+                    return new ContactDetailFragment();
                 case 1:
                     return new ContactDetailUpdatesFragment();
             }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 07c9a48..4b7c360 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -728,11 +728,6 @@
         }
 
         @Override
-        public void onDetailsLoaded(ContactLoader.Result result) {
-            // Nothing needs to be done here
-        }
-
-        @Override
         public void onEditRequested(Uri contactLookupUri) {
             startActivityForResult(
                     new Intent(Intent.ACTION_EDIT, contactLookupUri), SUBACTIVITY_EDIT_CONTACT);
diff --git a/src/com/android/contacts/detail/ContactDetailAboutFragment.java b/src/com/android/contacts/detail/ContactDetailAboutFragment.java
deleted file mode 100644
index fc6b9cb..0000000
--- a/src/com/android/contacts/detail/ContactDetailAboutFragment.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2011 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.detail;
-
-import com.android.contacts.ContactLoader;
-import com.android.contacts.R;
-
-import android.app.ActionBar;
-import android.app.Activity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-public class ContactDetailAboutFragment extends ContactDetailFragment {
-
-    private static final String TAG = "ContactDetailAboutFragment";
-
-    public ContactDetailAboutFragment() {
-        // Explicit constructor for inflation
-    }
-
-    @Override
-    protected View createNewHeaderView(ViewGroup parent) {
-        ViewGroup headerView = (ViewGroup) inflate(
-                R.layout.simple_contact_detail_header_view_list_item, parent, false);
-        TextView phoneticNameView = (TextView) headerView.findViewById(R.id.phonetic_name);
-        TextView attributionView = (TextView) headerView.findViewById(R.id.attribution);
-        ContactDetailDisplayUtils.setPhoneticName(getContext(), getContactData(), phoneticNameView);
-        ContactDetailDisplayUtils.setAttribution(getContext(), getContactData(), attributionView);
-        return headerView;
-    }
-
-    @Override
-    protected void bindData() {
-        ContactLoader.Result contactData = getContactData();
-        if (contactData != null) {
-            // Setup the activity title and subtitle with contact name and company
-            Activity activity = getActivity();
-            CharSequence displayName = ContactDetailDisplayUtils.getDisplayName(activity,
-                    contactData);
-            String company =  ContactDetailDisplayUtils.getCompany(activity, contactData);
-
-            ActionBar actionBar = activity.getActionBar();
-            actionBar.setTitle(displayName);
-            actionBar.setSubtitle(company);
-
-            // Pass the contact loader result to the listener to finish setup
-            Listener listener = getListener();
-            if (listener != null) {
-                listener.onDetailsLoaded(contactData);
-            }
-        }
-
-        super.bindData();
-    }
-}
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index 0f12de7..357350d 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -209,15 +209,43 @@
      */
     public static void setSocialSnippetAndDate(Context context, Result contactData,
             TextView statusView, TextView dateView) {
+        if (statusView == null || dateView == null) {
+            return;
+        }
         setDataOrHideIfNone(contactData.getSocialSnippet(), statusView);
         setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(contactData, context), dateView);
     }
 
     /**
+     * Sets the display name of this contact to the given {@link TextView}. If
+     * there is none, then set the view to gone.
+     */
+    public static void setDisplayName(Context context, Result contactData, TextView textView) {
+        if (textView == null) {
+            return;
+        }
+        setDataOrHideIfNone(getDisplayName(context, contactData), textView);
+    }
+
+    /**
+     * Sets the company and job title of this contact to the given {@link TextView}. If
+     * there is none, then set the view to gone.
+     */
+    public static void setCompanyName(Context context, Result contactData, TextView textView) {
+        if (textView == null) {
+            return;
+        }
+        setDataOrHideIfNone(getCompany(context, contactData), textView);
+    }
+
+    /**
      * Sets the phonetic name of this contact to the given {@link TextView}. If
      * there is none, then set the view to gone.
      */
     public static void setPhoneticName(Context context, Result contactData, TextView textView) {
+        if (textView == null) {
+            return;
+        }
         setDataOrHideIfNone(getPhoneticName(context, contactData), textView);
     }
 
@@ -226,6 +254,9 @@
      * there is none, then set the view to gone.
      */
     public static void setAttribution(Context context, Result contactData, TextView textView) {
+        if (textView == null) {
+            return;
+        }
         setDataOrHideIfNone(getAttribution(context, contactData), textView);
     }
 
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index c2aaea4..8ee22a4 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -21,6 +21,7 @@
 import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactOptionsActivity;
 import com.android.contacts.ContactPresenceIconUtil;
+import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.GroupMetaData;
 import com.android.contacts.NfcHandler;
@@ -40,6 +41,7 @@
 import com.android.internal.telephony.ITelephony;
 
 import android.accounts.Account;
+import android.app.ActionBar;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
@@ -100,6 +102,7 @@
 import android.widget.AdapterView.OnItemLongClickListener;
 import android.widget.BaseAdapter;
 import android.widget.Button;
+import android.widget.CheckBox;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
@@ -127,7 +130,7 @@
     private NfcHandler mNfcHandler;
 
     private ContactLoader.Result mContactData;
-    private ContactDetailHeaderView mHeaderView;
+    private ViewGroup mHeaderView;
     private ImageView mPhotoView;
     private ListView mListView;
     private ViewAdapter mAdapter;
@@ -139,6 +142,7 @@
     private int mNumPhoneNumbers = 0;
     private String mDefaultCountryIso;
     private boolean mContactDataDisplayed;
+    private boolean mContactPhotoDisplayedInHeader = true;
 
     private boolean mOptionsMenuOptions;
     private boolean mOptionsMenuEditable;
@@ -268,6 +272,10 @@
         });
 
         mView.setVisibility(View.INVISIBLE);
+
+        if (mContactData != null) {
+            bindData();
+        }
         return mView;
     }
 
@@ -359,6 +367,19 @@
         }
     }
 
+    /**
+     * Sets whether or not the contact photo should be shown in the list of contact details in this
+     * {@link Fragment}.
+     */
+    public void setShowPhotoInHeader(boolean showPhoto) {
+        mContactPhotoDisplayedInHeader = showPhoto;
+    }
+
+    public void setData(ContactLoader.Result result) {
+        mContactData = result;
+        bindData();
+    }
+
     protected void bindData() {
         if (mView == null) {
             return;
@@ -1061,7 +1082,49 @@
             if (mHeaderView != null) {
                 return mHeaderView;
             }
-            return createNewHeaderView(parent);
+
+            mHeaderView = (ViewGroup) inflate(
+                    R.layout.simple_contact_detail_header_view_list_item, parent, false);
+
+            TextView displayNameView = (TextView) mHeaderView.findViewById(R.id.name);
+            TextView companyView = (TextView) mHeaderView.findViewById(R.id.company);
+            TextView phoneticNameView = (TextView) mHeaderView.findViewById(R.id.phonetic_name);
+            TextView attributionView = (TextView) mHeaderView.findViewById(R.id.attribution);
+            ImageView photoView = (ImageView) mHeaderView.findViewById(R.id.photo);
+
+            ContactDetailDisplayUtils.setDisplayName(mContext, mContactData, displayNameView);
+            ContactDetailDisplayUtils.setCompanyName(mContext, mContactData, companyView);
+            ContactDetailDisplayUtils.setPhoneticName(mContext, mContactData, phoneticNameView);
+            ContactDetailDisplayUtils.setAttribution(mContext, mContactData, attributionView);
+
+            // Set the photo if it should be displayed
+            if (mContactPhotoDisplayedInHeader) {
+                ContactDetailDisplayUtils.setPhoto(mContext, mContactData, photoView);
+            } else {
+                // Otherwise hide the view
+                photoView.setVisibility(View.INVISIBLE);
+            }
+
+            // Set the starred state if it should be displayed
+            final CheckBox starredView = (CheckBox) mHeaderView.findViewById(R.id.star);
+            if (starredView != null) {
+                ContactDetailDisplayUtils.setStarred(mContactData, starredView);
+                final Uri lookupUri = mContactData.getLookupUri();
+                starredView.setOnClickListener(new OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        // Toggle "starred" state
+                        // Make sure there is a contact
+                        if (lookupUri != null) {
+                            Intent intent = ContactSaveService.createSetStarredIntent(
+                                    getContext(), lookupUri, starredView.isChecked());
+                            getContext().startService(intent);
+                        }
+                    }
+                });
+            }
+
+            return mHeaderView;
         }
 
         private View getSeparatorEntryView(View convertView, ViewGroup parent) {
@@ -1243,16 +1306,6 @@
         }
     }
 
-    /**
-     * Returns a new header view for the top of the list of contact details.
-     */
-    protected View createNewHeaderView(ViewGroup parent) {
-        mHeaderView = (ContactDetailHeaderView) inflate(
-                R.layout.contact_detail_header_view_list_item, parent, false);
-        mHeaderView.loadData(mContactData);
-        return mHeaderView;
-    }
-
     @Override
     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
         inflater.inflate(R.menu.view_contact, menu);
@@ -1504,11 +1557,6 @@
         public void onContactNotFound();
 
         /**
-         * This contact's details have been loaded.
-         */
-        public void onDetailsLoaded(ContactLoader.Result result);
-
-        /**
          * User decided to go to Edit-Mode
          */
         public void onEditRequested(Uri lookupUri);
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index b253b25..161ae32 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -154,6 +154,10 @@
      * from the outside to fully setup the View
      */
     public void loadData(ContactLoader.Result contactData) {
+        if (contactData == null) {
+            return;
+        }
+
         View aboutView = findViewById(R.id.tab_about);
         View updateView = findViewById(R.id.tab_update);
 
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index 0c4dc4f..05ae866 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.detail;
 
+import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
 
@@ -31,6 +32,8 @@
 
     private static final String TAG = "ContactDetailUpdatesFragment";
 
+    private ContactLoader.Result mContactData;
+
     /**
      * This optional view adds an alpha layer over the entire fragment.
      */
@@ -57,6 +60,11 @@
         return rootView;
     }
 
+    public void setData(ContactLoader.Result result) {
+        mContactData = result;
+        // TODO: Load up the "recent updates" section based on the result
+    }
+
     @Override
     public void setAlphaLayerValue(float alpha) {
         if (mAlphaLayer != null) {
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
new file mode 100644
index 0000000..3ece0ce
--- /dev/null
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 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.detail;
+
+import com.android.contacts.ContactLoader;
+import com.android.contacts.R;
+import com.android.internal.util.Objects;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * This is an invisible worker {@link Fragment} that loads the contact details for the contact card.
+ * The data is then passed to the listener, who can then pass the data to other {@link View}s.
+ */
+public class ContactLoaderFragment extends Fragment {
+
+    private static final String TAG = ContactLoaderFragment.class.getSimpleName();
+
+    /**
+     * This is a listener to the {@link ContactLoaderFragment} and will be notified when the
+     * contact details have finished loading.
+     */
+    public static interface ContactLoaderFragmentListener {
+        public void onDetailsLoaded(ContactLoader.Result result);
+    }
+
+    private static final int LOADER_DETAILS = 1;
+
+    private static final String KEY_CONTACT_URI = "contactUri";
+    private static final String LOADER_ARG_CONTACT_URI = "contactUri";
+
+    private Context mContext;
+    private Uri mLookupUri;
+    private ContactLoaderFragmentListener mListener;
+
+    private ContactLoader.Result mContactData;
+
+    public ContactLoaderFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        mContext = activity;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        // This is an empty view that is set to visibility gone.
+        return inflater.inflate(R.layout.contact_detail_loader_fragment, container, false);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        if (mLookupUri != null) {
+            Bundle args = new Bundle();
+            args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
+            getLoaderManager().initLoader(LOADER_DETAILS, args, mDetailLoaderListener);
+        }
+    }
+
+    public void loadUri(Uri lookupUri) {
+        if (Objects.equal(lookupUri, mLookupUri)) {
+            // Same URI, no need to load the data again
+            return;
+        }
+
+        mLookupUri = lookupUri;
+        if (mLookupUri == null) {
+            getLoaderManager().destroyLoader(LOADER_DETAILS);
+            mContactData = null;
+            if (mListener != null) {
+                mListener.onDetailsLoaded(mContactData);
+            }
+        } else if (getActivity() != null) {
+            Bundle args = new Bundle();
+            args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
+            getLoaderManager().restartLoader(LOADER_DETAILS, args, mDetailLoaderListener);
+        }
+    }
+
+    public void setListener(ContactLoaderFragmentListener value) {
+        mListener = value;
+    }
+
+    /**
+     * The listener for the detail loader
+     */
+    private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDetailLoaderListener =
+            new LoaderCallbacks<ContactLoader.Result>() {
+        @Override
+        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+            Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
+            return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+            if (!mLookupUri.equals(data.getUri())) {
+                return;
+            }
+
+            if (data != ContactLoader.Result.NOT_FOUND && data != ContactLoader.Result.ERROR) {
+                mContactData = data;
+            } else {
+                Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
+                mContactData = null;
+            }
+
+            if (mListener != null) {
+                mListener.onDetailsLoaded(mContactData);
+            }
+        }
+
+        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
+            mContactData = null;
+            if (mListener != null) {
+                mListener.onDetailsLoaded(mContactData);
+            }
+        }
+    };
+}