Reuse fragments when rotating contact card on phone

- Part 1 of refactoring contact card fragment code,
Part 2 will be refactoring the tablet code to fix
bug 5082871

- ContactDetailActivity should always have non-null
fragments after onCreate() (either retrieved from
Fragment Manager or created dynamically)

- New view pager adapter that returns views that contain
existing fragments (instead of generating new fragments)

- The main reason for these changes is that when we
create fragments dynamically (necessary for the ViewPager
otherwise the fragment from XML will already have a parent),
the view ID will be the pager view ID. When we rotate,
the fragments are already defined in XML for the fragment
carousel and already have different view IDs, so it won't
restore properly. We can't put the pager view ID in the
fragment carousel layout, otherwise the ContactDetailActivity
will try to cast it to a ViewPager. Alternatively, if we make
both layouts rely on dynamically created fragments, the problem
becomes the fact that once fragments are added to the fragment manager,
they cannot be retrieved and added again to a different parent if there's a
different layout (exception is thrown). Thus, the solution is to have
the same parent container in both phone portrait and landscape layouts.

- Also it is unclear what was happening to the fragments on
rotation (they weren't being restored but weren't being removed
from the FragmentManager). We can remove the hack now that
would store the ViewPager fragment tags in the saved instance bundle
and manually remove those fragments from the FragmentManager after
an orientation change.

- In onCreate() of the ContactDetailActivity, if this is a
restored instance, then inflate the correct layout right away
so the fragments can find the parent containers.

- Save/restore ListView state on rotation

- Save/restore selected tab for contact with social updates

- Clean up computation code for FragmentCarousel by moving it to
onMeasure() method

Bug: 4976082
Bug: 4686406
Bug: 5082871

Change-Id: I7840b1dd1110da4dcc28ebabe3fc2739ff11c2f2
diff --git a/res/layout/contact_detail_about_fragment_container.xml b/res/layout/contact_detail_about_fragment_container.xml
new file mode 100644
index 0000000..6fc9fe2
--- /dev/null
+++ b/res/layout/contact_detail_about_fragment_container.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<!--
+  Container for the "About" page fragment on the contact card for a contact with social updates.
+  This view ID must match with a view ID in the layout that is used after an orientation change.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/about_fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/res/layout/contact_detail_fragment_carousel.xml b/res/layout/contact_detail_fragment_carousel.xml
index 312fdf2..07c8d1c 100644
--- a/res/layout/contact_detail_fragment_carousel.xml
+++ b/res/layout/contact_detail_fragment_carousel.xml
@@ -21,13 +21,23 @@
     android:scrollbars="none"
     android:orientation="horizontal">
 
-    <fragment class="com.android.contacts.detail.ContactDetailFragment"
-        android:id="@+id/about_fragment"
+    <!--
+      Container for the "About" page fragment on the contact card for a contact
+      with social updates. This view ID must match with a view ID in the layout
+      that is used after an orientation change.
+    -->
+    <FrameLayout
+        android:id="@+id/about_fragment_container"
         android:layout_width="@dimen/detail_fragment_carousel_fragment_width"
         android:layout_height="match_parent" />
 
-    <fragment class="com.android.contacts.detail.ContactDetailUpdatesFragment"
-        android:id="@+id/updates_fragment"
+    <!--
+      Container for the "Updates" page fragment on the contact card for a contact
+      with social updates. This view ID must match with a view ID in the layout
+      that is used after an orientation change.
+    -->
+    <FrameLayout
+        android:id="@+id/updates_fragment_container"
         android:layout_width="@dimen/detail_fragment_carousel_fragment_width"
         android:layout_height="match_parent" />
 
diff --git a/res/layout/contact_detail_updates_fragment_container.xml b/res/layout/contact_detail_updates_fragment_container.xml
new file mode 100644
index 0000000..fb3a8ac
--- /dev/null
+++ b/res/layout/contact_detail_updates_fragment_container.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<!--
+  Container for the "Updates" page fragment on the contact card for a contact with social updates.
+  This view ID must match with a view ID in the layout that is used after an orientation change.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/updates_fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 02b7bac..3dffd71 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -25,6 +25,7 @@
 import com.android.contacts.detail.ContactDetailFragmentCarousel;
 import com.android.contacts.detail.ContactDetailTabCarousel;
 import com.android.contacts.detail.ContactDetailUpdatesFragment;
+import com.android.contacts.detail.ContactDetailViewPagerAdapter;
 import com.android.contacts.detail.ContactLoaderFragment;
 import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener;
 import com.android.contacts.interactions.ContactDeletionInteraction;
@@ -72,6 +73,8 @@
      */
     public static final String INTENT_KEY_IGNORE_DEFAULT_UP_BEHAVIOR = "ignoreDefaultUpBehavior";
 
+    private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates";
+    private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex";
     private static final String KEY_DETAIL_FRAGMENT_TAG = "detailFragTag";
     private static final String KEY_UPDATES_FRAGMENT_TAG = "updatesFragTag";
 
@@ -90,6 +93,8 @@
 
     private ContactDetailFragmentCarousel mFragmentCarousel;
 
+    private boolean mFragmentsAddedToFragmentManager;
+
     private ViewGroup mRootView;
     private ViewGroup mContentView;
     private LayoutInflater mInflater;
@@ -129,25 +134,37 @@
         mRootView = (ViewGroup) findViewById(R.id.contact_detail_view);
         mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
-        // 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).
+        FragmentManager fragmentManager = getFragmentManager();
+        mDetailFragment = (ContactDetailFragment)
+                fragmentManager.findFragmentByTag(
+                ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
+        mUpdatesFragment = (ContactDetailUpdatesFragment)
+                fragmentManager.findFragmentByTag(
+                ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
+
+        // If the fragment were found in the {@link FragmentManager} then we don't need to add
+        // it again.
+        if (mDetailFragment != null) {
+            mFragmentsAddedToFragmentManager = true;
+        } else {
+            // Otherwise, create the fragments dynamically and remember to add them to the
+            // {@link FragmentManager}.
+            mDetailFragment = new ContactDetailFragment();
+            mUpdatesFragment = new ContactDetailUpdatesFragment();
+            mFragmentsAddedToFragmentManager = false;
+        }
+
+        mDetailFragment.setListener(mFragmentListener);
+        mDetailFragment.setVerticalScrollListener(mVerticalScrollListener);
+        mDetailFragment.setData(mLookupUri, mContactData);
+        mUpdatesFragment.setData(mLookupUri, mContactData);
+
         if (savedState != null) {
-            String aboutFragmentTag = savedState.getString(KEY_DETAIL_FRAGMENT_TAG);
-            String updatesFragmentTag = savedState.getString(KEY_UPDATES_FRAGMENT_TAG);
-
-            FragmentManager fragmentManager = getFragmentManager();
-            mDetailFragment = (ContactDetailFragment) fragmentManager.findFragmentByTag(
-                    aboutFragmentTag);
-            mUpdatesFragment = (ContactDetailUpdatesFragment) fragmentManager.findFragmentByTag(
-                    updatesFragmentTag);
-
-            if (mDetailFragment != null && mUpdatesFragment != null) {
-                FragmentTransaction ft = fragmentManager.beginTransaction();
-                ft.remove(mDetailFragment);
-                ft.remove(mUpdatesFragment);
-                ft.commit();
+            mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES);
+            if (mContactHasUpdates) {
+                setupContactWithUpdates(savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0));
+            } else {
+                setupContactWithoutUpdates();
             }
         }
 
@@ -166,16 +183,9 @@
 
     @Override
     public void onAttachFragment(Fragment fragment) {
-        if (fragment instanceof ContactDetailFragment) {
-            mDetailFragment = (ContactDetailFragment) fragment;
-            mDetailFragment.setListener(mFragmentListener);
-            mDetailFragment.setVerticalScrollListener(mVerticalScrollListener);
-            mDetailFragment.setData(mLookupUri, mContactData);
-        } else if (fragment instanceof ContactDetailUpdatesFragment) {
-            mUpdatesFragment = (ContactDetailUpdatesFragment) fragment;
-            mUpdatesFragment.setData(mLookupUri, mContactData);
-        } else if (fragment instanceof ContactLoaderFragment) {
+         if (fragment instanceof ContactLoaderFragment) {
             mLoaderFragment = (ContactLoaderFragment) fragment;
+            mLoaderFragment.setRetainInstance(true);
             mLoaderFragment.setListener(mLoaderFragmentListener);
             mLoaderFragment.loadUri(getIntent().getData());
         }
@@ -261,6 +271,8 @@
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
+        outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates);
+        outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPage());
         if (mViewPager != null) {
             outState.putString(KEY_DETAIL_FRAGMENT_TAG, mDetailFragment.getTag());
             outState.putString(KEY_UPDATES_FRAGMENT_TAG, mUpdatesFragment.getTag());
@@ -296,7 +308,7 @@
                     invalidateOptionsMenu();
                     setupTitle();
                     if (mContactHasUpdates) {
-                        setupContactWithUpdates();
+                        setupContactWithUpdates(null /* Don't change the current page */);
                     } else {
                         setupContactWithoutUpdates();
                     }
@@ -328,7 +340,11 @@
         actionBar.setSubtitle(company);
     }
 
-    private void setupContactWithUpdates() {
+    /**
+     * Setup the layout for the contact with updates. Pass in the index of the current page to
+     * select or null if the current selection should be left as is.
+     */
+    private void setupContactWithUpdates(Integer currentPageIndex) {
         if (mContentView == null) {
             mContentView = (ViewGroup) mInflater.inflate(
                     R.layout.contact_detail_container_with_updates, mRootView, false);
@@ -337,29 +353,78 @@
             // Make sure all needed views are retrieved. Note that narrow width screens have a
             // {@link ViewPager} and {@link ContactDetailTabCarousel}, while wide width screens have
             // a {@link ContactDetailFragmentCarousel}.
-            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);
             }
 
+            mViewPager = (ViewPager) findViewById(R.id.pager);
+            if (mViewPager != null) {
+                // Inflate 2 view containers to pass in as children to the {@link ViewPager},
+                // which will in turn be the parents to the mDetailFragment and mUpdatesFragment
+                // since the fragments must have the same parent view IDs in both landscape and
+                // portrait layouts.
+                ViewGroup detailContainer = (ViewGroup) mInflater.inflate(
+                        R.layout.contact_detail_about_fragment_container, mViewPager, false);
+                ViewGroup updatesContainer = (ViewGroup) mInflater.inflate(
+                        R.layout.contact_detail_updates_fragment_container, mViewPager, false);
+
+                ContactDetailViewPagerAdapter adapter = new ContactDetailViewPagerAdapter();
+                adapter.setAboutFragmentView(detailContainer);
+                adapter.setUpdatesFragmentView(updatesContainer);
+
+                mViewPager.addView(detailContainer);
+                mViewPager.addView(updatesContainer);
+                mViewPager.setAdapter(adapter);
+                mViewPager.setOnPageChangeListener(mOnPageChangeListener);
+
+                if (!mFragmentsAddedToFragmentManager) {
+                    FragmentManager fragmentManager = getFragmentManager();
+                    FragmentTransaction transaction = fragmentManager.beginTransaction();
+                    transaction.add(R.id.about_fragment_container, mDetailFragment,
+                            ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
+                    transaction.add(R.id.updates_fragment_container, mUpdatesFragment,
+                            ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
+                    transaction.commit();
+                    fragmentManager.executePendingTransactions();
+                }
+
+                // Select page if applicable
+                if (currentPageIndex != null) {
+                    mViewPager.setCurrentItem(currentPageIndex);
+                }
+            }
+
             mFragmentCarousel = (ContactDetailFragmentCarousel)
                     findViewById(R.id.fragment_carousel);
+            // Add the fragments to the fragment containers in the carousel using a
+            // {@link FragmentTransaction} if they haven't already been added to the
+            // {@link FragmentManager}.
+            if (mFragmentCarousel != null) {
+                if (!mFragmentsAddedToFragmentManager) {
+                    FragmentManager fragmentManager = getFragmentManager();
+                    FragmentTransaction transaction = fragmentManager.beginTransaction();
+                    transaction.add(R.id.about_fragment_container, mDetailFragment,
+                            ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG);
+                    transaction.add(R.id.updates_fragment_container, mUpdatesFragment,
+                            ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG);
+                    transaction.commit();
+                    fragmentManager.executePendingTransactions();
+                }
+
+                // Select page if applicable
+                if (currentPageIndex != null) {
+                    mFragmentCarousel.setCurrentPage(currentPageIndex);
+                }
+            }
         }
 
         // Then reset the contact data to the appropriate views
         if (mTabCarousel != null) {
             mTabCarousel.loadData(mContactData);
         }
-        if (mFragmentCarousel != null) {
-            if (mDetailFragment != null) mFragmentCarousel.setAboutFragment(mDetailFragment);
-            if (mUpdatesFragment != null) mFragmentCarousel.setUpdatesFragment(mUpdatesFragment);
+        if (mFragmentCarousel != null && mDetailFragment != null && mUpdatesFragment != null) {
+            mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment);
         }
         if (mDetailFragment != null) {
             mDetailFragment.setData(mLookupUri, mContactData);
@@ -374,6 +439,8 @@
             mContentView = (ViewGroup) mInflater.inflate(
                     R.layout.contact_detail_container_without_updates, mRootView, false);
             mRootView.addView(mContentView);
+            mDetailFragment = (ContactDetailFragment) getFragmentManager().findFragmentById(
+                    R.id.contact_detail_fragment);
         }
         // Reset contact data
         if (mDetailFragment != null) {
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index f550a0f..02394f2 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -61,6 +61,7 @@
 import android.net.Uri;
 import android.net.WebAddress;
 import android.os.Bundle;
+import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.provider.ContactsContract;
@@ -130,6 +131,7 @@
     }
 
     private static final String KEY_CONTACT_URI = "contactUri";
+    private static final String KEY_LIST_STATE = "liststate";
     private static final String LOADER_ARG_CONTACT_URI = "contactUri";
 
     private Context mContext;
@@ -193,6 +195,12 @@
     private View mTouchInterceptLayer;
 
     /**
+     * Saved state of the {@link ListView}. This must be saved and applied to the {@ListView} only
+     * when the adapter has been populated again.
+     */
+    private Parcelable mListState;
+
+    /**
      * A list of distinct contact IDs included in the current contact.
      */
     private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
@@ -224,6 +232,7 @@
         super.onCreate(savedInstanceState);
         if (savedInstanceState != null) {
             mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
+            mListState = savedInstanceState.getParcelable(KEY_LIST_STATE);
         }
         mNfcHandler = new NfcHandler(this);
     }
@@ -232,6 +241,9 @@
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
+        if (mListView != null) {
+            outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
+        }
     }
 
     @Override
@@ -286,6 +298,7 @@
         if (mContactData != null) {
             bindData();
         }
+
         return mView;
     }
 
@@ -415,9 +428,16 @@
         if (mAdapter == null) {
             mAdapter = new ViewAdapter();
             mListView.setAdapter(mAdapter);
-        } else {
-            mAdapter.notifyDataSetChanged();
         }
+
+        // Restore {@link ListView} state if applicable because the adapter is now populated.
+        if (mListState != null) {
+            mListView.onRestoreInstanceState(mListState);
+            mListState = null;
+        }
+
+        mAdapter.notifyDataSetChanged();
+
         mListView.setEmptyView(mEmptyView);
 
         configureQuickFix();
diff --git a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
index 95cf055..eb1d1f0 100644
--- a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
@@ -19,6 +19,7 @@
 import com.android.contacts.R;
 
 import android.content.Context;
+import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -36,8 +37,21 @@
 
     private static final String TAG = ContactDetailFragmentCarousel.class.getSimpleName();
 
+    /**
+     * Number of pixels that this view can be scrolled horizontally.
+     */
     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
+
+    /**
+     * Minimum X scroll position that must be surpassed (if the user is on the "about" page of the
+     * contact card), in order for this view to automatically snap to the "updates" page.
+     */
     private int mLowerThreshold = Integer.MIN_VALUE;
+
+    /**
+     * Maximum X scroll position (if the user is on the "updates" page of the contact card), below
+     * which this view will automatically snap to the "about" page.
+     */
     private int mUpperThreshold = Integer.MIN_VALUE;
 
     private static final int ABOUT_PAGE = 0;
@@ -51,6 +65,9 @@
 
     private static final float MAX_ALPHA = 0.5f;
 
+    private final Handler mHandler = new Handler();
+    private boolean mAttachedToWindow;
+
     public ContactDetailFragmentCarousel(Context context) {
         this(context, null);
     }
@@ -69,19 +86,52 @@
         setOnTouchListener(this);
     }
 
-    public void setAboutFragment(ViewOverlay fragment) {
-        // TODO: We can't always assume the "about" page will be the current page.
-        mAboutFragment = fragment;
-        mAboutFragment.enableAlphaLayer();
-        mAboutFragment.setAlphaLayerValue(0);
-        mAboutFragment.disableTouchInterceptor();
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // Take the width of this view as the width of the screen and compute necessary thresholds.
+        // Only do this computation 1x.
+        if (mAllowedHorizontalScrollLength == Integer.MIN_VALUE) {
+            int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
+            int fragmentWidth = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.detail_fragment_carousel_fragment_width);
+            mAllowedHorizontalScrollLength = (2 * fragmentWidth) - screenWidth;
+            mLowerThreshold = (screenWidth - fragmentWidth) / 2;
+            mUpperThreshold = mAllowedHorizontalScrollLength - mLowerThreshold;
+
+            // Snap to the current page now that the allowed horizontal scroll length has been
+            // computed.
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mAttachedToWindow && mAboutFragment != null && mUpdatesFragment != null) {
+                        snapToEdge();
+                    }
+                }
+            });
+        }
     }
 
-    public void setUpdatesFragment(ViewOverlay fragment) {
-        mUpdatesFragment = fragment;
+    public void setCurrentPage(int pageIndex) {
+        if (mCurrentPage != pageIndex) {
+            mCurrentPage = pageIndex;
+            if (mAttachedToWindow && mAboutFragment != null && mUpdatesFragment != null) {
+                snapToEdge();
+            }
+        }
+    }
+
+    public void setFragments(ViewOverlay aboutFragment, ViewOverlay updatesFragment) {
+        mAboutFragment = aboutFragment;
+        mAboutFragment.enableAlphaLayer();
+        mAboutFragment.setAlphaLayerValue(mCurrentPage == ABOUT_PAGE ? 0 : MAX_ALPHA);
+
+        mUpdatesFragment = updatesFragment;
         mUpdatesFragment.enableAlphaLayer();
-        mUpdatesFragment.setAlphaLayerValue(MAX_ALPHA);
-        mUpdatesFragment.enableTouchInterceptor(mUpdatesFragTouchInterceptListener);
+        mUpdatesFragment.setAlphaLayerValue(mCurrentPage == UPDATES_PAGE ? 0 : MAX_ALPHA);
+
+        snapToEdge();
     }
 
     public int getCurrentPage() {
@@ -121,9 +171,9 @@
 
     private void updateAlphaLayers() {
         mAboutFragment.setAlphaLayerValue(mLastScrollPosition * MAX_ALPHA /
-                getAllowedHorizontalScrollLength());
+                mAllowedHorizontalScrollLength);
         mUpdatesFragment.setAlphaLayerValue(MAX_ALPHA - mLastScrollPosition * MAX_ALPHA /
-                getAllowedHorizontalScrollLength());
+                mAllowedHorizontalScrollLength);
     }
 
     @Override
@@ -139,7 +189,7 @@
                 smoothScrollTo(0, 0);
                 break;
             case UPDATES_PAGE:
-                smoothScrollTo(getAllowedHorizontalScrollLength(), 0);
+                smoothScrollTo(mAllowedHorizontalScrollLength, 0);
                 break;
         }
         updateTouchInterceptors();
@@ -154,59 +204,15 @@
             case ABOUT_PAGE:
                 // If the user is on the "about" page, and the scroll position exceeds the lower
                 // threshold, then we should switch to the "updates" page.
-                return (mLastScrollPosition > getLowerThreshold()) ? UPDATES_PAGE : ABOUT_PAGE;
+                return (mLastScrollPosition > mLowerThreshold) ? UPDATES_PAGE : ABOUT_PAGE;
             case UPDATES_PAGE:
                 // If the user is on the "updates" page, and the scroll position goes below the
                 // upper threshold, then we should switch to the "about" page.
-                return (mLastScrollPosition < getUpperThreshold()) ? ABOUT_PAGE : UPDATES_PAGE;
+                return (mLastScrollPosition < mUpperThreshold) ? ABOUT_PAGE : UPDATES_PAGE;
         }
         throw new IllegalStateException("Invalid current page " + mCurrentPage);
     }
 
-    /**
-     * Returns the number of pixels that this view can be scrolled horizontally.
-     */
-    private int getAllowedHorizontalScrollLength() {
-        if (mAllowedHorizontalScrollLength == Integer.MIN_VALUE) {
-            computeThresholds();
-        }
-        return mAllowedHorizontalScrollLength;
-    }
-
-    /**
-     * Returns the minimum X scroll position that must be surpassed (if the user is on the "about"
-     * page of the contact card), in order for this view to automatically snap to the "updates"
-     * page.
-     */
-    private int getLowerThreshold() {
-        if (mLowerThreshold == Integer.MIN_VALUE) {
-            computeThresholds();
-        }
-        return mLowerThreshold;
-    }
-
-    /**
-     * Returns the maximum X scroll position (if the user is on the "updates" page of the contact
-     * card), below which this view will automatically snap to the "about" page.
-     */
-    private int getUpperThreshold() {
-        if (mLowerThreshold == Integer.MIN_VALUE) {
-            computeThresholds();
-        }
-        return mUpperThreshold;
-    }
-
-    // TODO: Move this to a Fragment override method (i.e. onActivityCreated or some method where
-    // we can be sure the width of the views are non-zero) instead of doing it on the fly when the
-    // values are requested for the first time.
-    private void computeThresholds() {
-        int screenWidth = getWidth();
-        int fragmentWidth = findViewById(R.id.about_fragment).getWidth();
-        mAllowedHorizontalScrollLength = (2 * fragmentWidth) - screenWidth;
-        mLowerThreshold = (screenWidth - fragmentWidth) / 2;
-        mUpperThreshold = mAllowedHorizontalScrollLength - mLowerThreshold;
-    }
-
     @Override
     public boolean onTouch(View v, MotionEvent event) {
         if (event.getAction() == MotionEvent.ACTION_UP) {
@@ -216,4 +222,16 @@
         }
         return false;
     }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAttachedToWindow = true;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mAttachedToWindow = false;
+    }
 }
diff --git a/src/com/android/contacts/detail/ContactDetailViewPagerAdapter.java b/src/com/android/contacts/detail/ContactDetailViewPagerAdapter.java
new file mode 100644
index 0000000..39544df
--- /dev/null
+++ b/src/com/android/contacts/detail/ContactDetailViewPagerAdapter.java
@@ -0,0 +1,104 @@
+/*
+ * 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 android.os.Parcelable;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Adapter for the {@link ViewPager} for the contact detail page for a contact with social updates.
+ */
+public class ContactDetailViewPagerAdapter extends PagerAdapter {
+
+    public static final String ABOUT_FRAGMENT_TAG = "view-pager-about-fragment";
+    public static final String UPDTES_FRAGMENT_TAG = "view-pager-updates-fragment";
+
+    private static final int FRAGMENT_COUNT = 2;
+
+    private static final int INDEX_ABOUT_FRAGMENT = 0;
+    private static final int INDEX_UPDATES_FRAGMENT = 1;
+
+    private ViewGroup mAboutFragmentView;
+    private ViewGroup mUpdatesFragmentView;
+
+    public ContactDetailViewPagerAdapter() {
+    }
+
+    public void setAboutFragmentView(ViewGroup view) {
+        mAboutFragmentView = view;
+    }
+
+    public void setUpdatesFragmentView(ViewGroup view) {
+        mUpdatesFragmentView = view;
+    }
+
+    @Override
+    public int getCount() {
+        return FRAGMENT_COUNT;
+    }
+
+    /** Gets called when the number of items changes. */
+    @Override
+    public int getItemPosition(Object object) {
+        if (object == mAboutFragmentView) {
+            return INDEX_ABOUT_FRAGMENT;
+        }
+        if (object == mUpdatesFragmentView) {
+            return INDEX_UPDATES_FRAGMENT;
+        }
+        return POSITION_NONE;
+    }
+
+    @Override
+    public void startUpdate(View container) {
+    }
+
+    @Override
+    public Object instantiateItem(View container, int position) {
+        switch (position) {
+            case INDEX_ABOUT_FRAGMENT:
+                return mAboutFragmentView;
+            case INDEX_UPDATES_FRAGMENT:
+                return mUpdatesFragmentView;
+        }
+        throw new IllegalArgumentException("Invalid position: " + position);
+    }
+
+    @Override
+    public void destroyItem(View container, int position, Object object) {
+    }
+
+    @Override
+    public void finishUpdate(View container) {
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        return ((View) object) == view;
+    }
+
+    @Override
+    public Parcelable saveState() {
+        return null;
+    }
+
+    @Override
+    public void restoreState(Parcelable state, ClassLoader loader) {
+    }
+}