Merge "Allow setting photo on otherwise-empty local contacts."
diff --git a/res/layout/carousel_about_tab.xml b/res/layout/carousel_about_tab.xml
index 0f93482..f2e504b 100644
--- a/res/layout/carousel_about_tab.xml
+++ b/res/layout/carousel_about_tab.xml
@@ -58,12 +58,4 @@
         android:textColor="@color/detail_tab_carousel_tab_label_color"
         style="@android:style/Widget.Holo.ActionBar.TabView" />
 
-    <View
-        android:id="@+id/touch_intercept_overlay"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_alignParentLeft="true"
-        android:layout_alignParentTop="true"
-        android:background="?android:attr/selectableItemBackground"
-        android:visibility="gone"/>
 </view>
diff --git a/res/layout/carousel_updates_tab.xml b/res/layout/carousel_updates_tab.xml
index e4b61b9..93e6e8f 100644
--- a/res/layout/carousel_updates_tab.xml
+++ b/res/layout/carousel_updates_tab.xml
@@ -81,13 +81,4 @@
         android:textColor="@color/detail_tab_carousel_tab_label_color"
         style="@android:style/Widget.Holo.ActionBar.TabView" />
 
-    <View
-        android:id="@+id/touch_intercept_overlay"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_alignParentLeft="true"
-        android:layout_alignParentTop="true"
-        android:background="?android:attr/selectableItemBackground"
-        android:visibility="gone"/>
-
 </view>
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index bc0f3f4..aed377b 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -412,9 +412,9 @@
 
         /**
          * @return true if this is a contact (not group, etc.) with at least one
-         *         writeable raw-contact, and false otherwise.
+         *         writable raw-contact, and false otherwise.
          */
-        public boolean isWritableContact(Context context) {
+        public boolean isWritableContact(final Context context) {
             if (isDirectoryEntry()) return false;
             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
             for (Entity rawContact : getEntities()) {
diff --git a/src/com/android/contacts/ContactPhotoManager.java b/src/com/android/contacts/ContactPhotoManager.java
index 4286293..5b6a1ca 100644
--- a/src/com/android/contacts/ContactPhotoManager.java
+++ b/src/com/android/contacts/ContactPhotoManager.java
@@ -582,8 +582,11 @@
             view.setImageBitmap(cachedBitmap);
         }
 
-        // Put the bitmap in the LRU cache
-        mBitmapCache.put(request.getKey(), cachedBitmap);
+        // Put the bitmap in the LRU cache. But only do this for images that are small enough
+        // (we require that at least six of those can be cached at the same time)
+        if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
+            mBitmapCache.put(request.getKey(), cachedBitmap);
+        }
 
         // Soften the reference
         holder.bitmap = null;
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index affdd1d..edc631f 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -59,7 +59,6 @@
     private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
     /** The token for the query to mark all missed calls as read after seeing the call log. */
     private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
-
     /** The token for the query to fetch voicemail status messages. */
     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
 
@@ -75,6 +74,24 @@
     @GuardedBy("this") private Cursor mNewCallsCursor;
     /** The cursor containing the old calls, or null if they have not yet been fetched. */
     @GuardedBy("this") private Cursor mOldCallsCursor;
+    /**
+     * The identifier of the latest calls request.
+     * <p>
+     * A request for the list of calls requires two queries and hence the two cursor
+     * {@link #mNewCallsCursor} and {@link #mOldCallsCursor} above, corresponding to
+     * {@link #QUERY_NEW_CALLS_TOKEN} and {@link #QUERY_OLD_CALLS_TOKEN}.
+     * <p>
+     * When a new request is about to be started, existing cursors are closed. However, it is
+     * possible that one of the queries completes after the new request has started. This means that
+     * we might merge two cursors that do not correspond to the same request. Moreover, this may
+     * lead to a resource leak if the same query completes and we override the cursor without
+     * closing it first.
+     * <p>
+     * To make sure we only join two cursors from the same request, we use this variable to store
+     * the request id of the latest request and make sure we only process cursors corresponding to
+     * the this request.
+     */
+    @GuardedBy("this") private int mCallsRequestId;
 
     /**
      * Simple handler that wraps background calls to catch
@@ -141,9 +158,9 @@
      */
     public void fetchAllCalls() {
         cancelFetch();
-        invalidate();
-        fetchCalls(QUERY_NEW_CALLS_TOKEN, true /*isNew*/, false /*voicemailOnly*/);
-        fetchCalls(QUERY_OLD_CALLS_TOKEN, false /*isNew*/, false /*voicemailOnly*/);
+        int requestId = newCallsRequest();
+        fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, false /*voicemailOnly*/);
+        fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, false /*voicemailOnly*/);
     }
 
     /**
@@ -153,9 +170,9 @@
      */
     public void fetchVoicemailOnly() {
         cancelFetch();
-        invalidate();
-        fetchCalls(QUERY_NEW_CALLS_TOKEN, true /*isNew*/, true /*voicemailOnly*/);
-        fetchCalls(QUERY_OLD_CALLS_TOKEN, false /*isNew*/, true /*voicemailOnly*/);
+        int requestId = newCallsRequest();
+        fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, true /*voicemailOnly*/);
+        fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, true /*voicemailOnly*/);
     }
 
 
@@ -165,7 +182,7 @@
     }
 
     /** Fetches the list of calls in the call log, either the new one or the old ones. */
-    private void fetchCalls(int token, boolean isNew, boolean voicemailOnly) {
+    private void fetchCalls(int token, int requestId, boolean isNew, boolean voicemailOnly) {
         // We need to check for NULL explicitly otherwise entries with where READ is NULL
         // may not match either the query or its negation.
         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
@@ -182,7 +199,7 @@
             selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
             selectionArgs.add(Integer.toString(Calls.VOICEMAIL_TYPE));
         }
-        startQuery(token, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+        startQuery(token, requestId, Calls.CONTENT_URI_WITH_VOICEMAIL,
                 CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
                 Calls.DEFAULT_SORT_ORDER);
     }
@@ -239,25 +256,41 @@
     }
 
     /**
-     * Invalidate the current list of calls.
+     * Start a new request and return its id. The request id will be used as the cookie for the
+     * background request.
      * <p>
-     * This method is synchronized because it must close the cursors and reset them atomically.
+     * Closes any open cursor that has not yet been sent to the requester.
      */
-    private synchronized void invalidate() {
+    private synchronized int newCallsRequest() {
         MoreCloseables.closeQuietly(mNewCallsCursor);
         MoreCloseables.closeQuietly(mOldCallsCursor);
         mNewCallsCursor = null;
         mOldCallsCursor = null;
+        return ++mCallsRequestId;
     }
 
     @Override
     protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
         if (token == QUERY_NEW_CALLS_TOKEN) {
+            int requestId = ((Integer) cookie).intValue();
+            if (requestId != mCallsRequestId) {
+                // Ignore this query since it does not correspond to the latest request.
+                return;
+            }
+
             // Store the returned cursor.
+            MoreCloseables.closeQuietly(mNewCallsCursor);
             mNewCallsCursor = new ExtendedCursor(
                     cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
         } else if (token == QUERY_OLD_CALLS_TOKEN) {
+            int requestId = ((Integer) cookie).intValue();
+            if (requestId != mCallsRequestId) {
+                // Ignore this query since it does not correspond to the latest request.
+                return;
+            }
+
             // Store the returned cursor.
+            MoreCloseables.closeQuietly(mOldCallsCursor);
             mOldCallsCursor = new ExtendedCursor(
                     cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
         } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
diff --git a/src/com/android/contacts/detail/CarouselTab.java b/src/com/android/contacts/detail/CarouselTab.java
index dc564a9..9bdf98b 100644
--- a/src/com/android/contacts/detail/CarouselTab.java
+++ b/src/com/android/contacts/detail/CarouselTab.java
@@ -17,6 +17,7 @@
 package com.android.contacts.detail;
 
 import com.android.contacts.R;
+import com.android.contacts.util.ThemeUtils;
 
 import android.content.Context;
 import android.util.AttributeSet;
@@ -32,6 +33,8 @@
 
     private static final String TAG = CarouselTab.class.getSimpleName();
 
+    private static final boolean DEBUG = false;
+
     private static final long FADE_TRANSITION_TIME = 150;
 
     private TextView mLabelView;
@@ -50,6 +53,21 @@
 
     public CarouselTab(Context context, AttributeSet attrs) {
         super(context, attrs);
+
+        // Programmatically create and initialize touch-interceptor View.
+        mTouchInterceptLayer = new View(context);
+
+        LayoutParams layoutParams =
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+        layoutParams.addRule(ALIGN_PARENT_LEFT, TRUE);
+        layoutParams.addRule(ALIGN_PARENT_TOP, TRUE);
+        int background = ThemeUtils.getSelectableItemBackground(context.getTheme());
+
+        mTouchInterceptLayer.setVisibility(GONE);
+        mTouchInterceptLayer.setBackgroundResource(background);
+        mTouchInterceptLayer.setLayoutParams(layoutParams);
+
+        addView(mTouchInterceptLayer);
     }
 
     @Override
@@ -59,7 +77,9 @@
         mLabelView = (TextView) findViewById(R.id.label);
         mLabelBackgroundView = findViewById(R.id.label_background);
         mAlphaLayer = findViewById(R.id.alpha_overlay);
-        mTouchInterceptLayer = findViewById(R.id.touch_intercept_overlay);
+
+        mTouchInterceptLayer.bringToFront();
+        if (DEBUG) mTouchInterceptLayer.setBackgroundColor(0x4400FF00);
     }
 
     public void setLabel(String label) {
@@ -75,18 +95,18 @@
     }
 
     @Override
-    public void disableTouchInterceptor() {
-        if (mTouchInterceptLayer != null) {
-            mTouchInterceptLayer.setVisibility(View.GONE);
-        }
+    public void setTouchInterceptorListener(OnClickListener listener) {
+        mTouchInterceptLayer.setOnClickListener(listener);
     }
 
     @Override
-    public void enableTouchInterceptor(OnClickListener clickListener) {
-        if (mTouchInterceptLayer != null) {
-            mTouchInterceptLayer.setVisibility(View.VISIBLE);
-            mTouchInterceptLayer.setOnClickListener(clickListener);
-        }
+    public void disableTouchInterceptor() {
+        mTouchInterceptLayer.setVisibility(View.GONE);
+    }
+
+    @Override
+    public void enableTouchInterceptor() {
+        mTouchInterceptLayer.setVisibility(View.VISIBLE);
     }
 
     @Override
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index d5b04d9..238055c 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -317,7 +317,8 @@
 
     @Override
     public void setAlphaLayerValue(float alpha) {
-        // If the alpha layer is not ready yet, store it for later when the view is initialized
+        // If the alpha layer is not ready yet, store it for later when the view
+        // is initialized
         if (mAlphaLayer == null) {
             mInitialAlphaValue = alpha;
         } else {
@@ -327,10 +328,16 @@
     }
 
     @Override
-    public void enableTouchInterceptor(OnClickListener clickListener) {
+    public void setTouchInterceptorListener(OnClickListener listener) {
+        if (mTouchInterceptLayer != null) {
+            mTouchInterceptLayer.setOnClickListener(listener);
+        }
+    }
+
+    @Override
+    public void enableTouchInterceptor() {
         if (mTouchInterceptLayer != null) {
             mTouchInterceptLayer.setVisibility(View.VISIBLE);
-            mTouchInterceptLayer.setOnClickListener(clickListener);
         }
     }
 
@@ -442,7 +449,11 @@
                         mContext, mContactData, photoView, false);
                 if (mPhotoTouchOverlay != null) {
                     mPhotoTouchOverlay.setVisibility(View.VISIBLE);
-                    mPhotoTouchOverlay.setOnClickListener(listener);
+                    if (mContactData.isWritableContact(mContext)) {
+                        mPhotoTouchOverlay.setOnClickListener(listener);
+                    } else {
+                        mPhotoTouchOverlay.setClickable(false);
+                    }
                 }
             } else {
                 mStaticPhotoContainer.setVisibility(View.GONE);
@@ -1363,15 +1374,21 @@
         }
 
         @Override
+        public void setTouchInterceptorListener(OnClickListener listener) {
+            if (photoOverlayView != null) {
+                photoOverlayView.setOnClickListener(listener);
+            }
+        }
+
+        @Override
         public void setAlphaLayerValue(float alpha) {
             // Nothing to do.
         }
 
         @Override
-        public void enableTouchInterceptor(OnClickListener clickListener) {
+        public void enableTouchInterceptor() {
             if (photoOverlayView != null) {
                 photoOverlayView.setVisibility(View.VISIBLE);
-                photoOverlayView.setOnClickListener(clickListener);
             }
         }
 
@@ -1495,7 +1512,11 @@
                 final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
                 OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
                         mContext, mContactData, viewCache.photoView, expandOnClick);
-                viewCache.enableTouchInterceptor(listener);
+
+                if (expandOnClick || mContactData.isWritableContact(mContext)) {
+                    viewCache.setTouchInterceptorListener(listener);
+                    viewCache.enableTouchInterceptor();
+                }
             }
 
             // Set the starred state if it should be displayed
diff --git a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
index f9b057b..a83409f 100644
--- a/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailFragmentCarousel.java
@@ -169,6 +169,9 @@
     public void setFragments(ViewOverlay aboutFragment, ViewOverlay updatesFragment) {
         mAboutFragment = aboutFragment;
         mUpdatesFragment = updatesFragment;
+
+        mAboutFragment.setTouchInterceptorListener(mAboutFragTouchInterceptListener);
+        mUpdatesFragment.setTouchInterceptorListener(mUpdatesFragTouchInterceptListener);
     }
 
     /**
@@ -216,11 +219,11 @@
                 // The "about this contact" page has been selected, so disable the touch interceptor
                 // on this page and enable it for the "updates" page.
                 mAboutFragment.disableTouchInterceptor();
-                mUpdatesFragment.enableTouchInterceptor(mUpdatesFragTouchInterceptListener);
+                mUpdatesFragment.enableTouchInterceptor();
                 break;
             case UPDATES_PAGE:
                 mUpdatesFragment.disableTouchInterceptor();
-                mAboutFragment.enableTouchInterceptor(mAboutFragTouchInterceptListener);
+                mAboutFragment.enableTouchInterceptor();
                 break;
         }
     }
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index be07e7a..e5ce961 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -36,7 +36,6 @@
 import android.os.Bundle;
 import android.support.v4.view.ViewPager;
 import android.support.v4.view.ViewPager.OnPageChangeListener;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewPropertyAnimator;
@@ -58,9 +57,6 @@
 
     private final int SINGLE_PANE_FADE_IN_DURATION = 275;
 
-    private static final String TAG = "ContactDetailLayoutController";
-    private static final boolean DEBUG = false;
-
     /**
      * There are 3 possible layouts for the contact detail screen:
      * 1. TWO_COLUMN - Tall and wide screen so the 2 pages can be shown side-by-side
@@ -135,13 +131,10 @@
         // Determine the layout mode based on the presence of certain views in the layout XML.
         if (mViewPager != null) {
             mLayoutMode = LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL;
-            if (DEBUG) Log.d(TAG, "set layout mode to VIEW_PAGER_AND_TAB_CAROUSEL");
         } else if (mFragmentCarousel != null) {
             mLayoutMode = LayoutMode.FRAGMENT_CAROUSEL;
-            if (DEBUG) Log.d(TAG, "set layout mode to FRAGMENT_CAROUSEL");
         } else {
             mLayoutMode = LayoutMode.TWO_COLUMN;
-            if (DEBUG) Log.d(TAG, "set layout mode to TWO_COLUMN");
         }
 
         initialize(savedState);
@@ -491,9 +484,8 @@
             // these scroll changes to the tab carousel. Ignore these events though if the carousel
             // is actually controlling the {@link ViewPager} scrolls because it will already be
             // in the correct position.
-            if (mViewPager.isFakeDragging()) {
-                return;
-            }
+            if (mViewPager.isFakeDragging()) return;
+
             int x = (int) ((position + positionOffset) *
                     mTabCarousel.getAllowedHorizontalScrollLength());
             mTabCarousel.scrollTo(x, 0);
@@ -626,34 +618,31 @@
         }
     };
 
-    private final ContactDetailTabCarousel.Listener mTabCarouselListener =
-            new ContactDetailTabCarousel.Listener() {
+    private final ContactDetailTabCarousel.Listener mTabCarouselListener 
+            = new ContactDetailTabCarousel.Listener() {
 
         @Override
         public void onTouchDown() {
-            // The user just started scrolling the carousel, so begin "fake dragging" the
-            // {@link ViewPager} if it's not already doing so.
-            if (mViewPager.isFakeDragging()) {
-                return;
-            }
-            mViewPager.beginFakeDrag();
+            // The user just started scrolling the carousel, so begin
+            // "fake dragging" the {@link ViewPager} if it's not already
+            // doing so.
+            if (!mViewPager.isFakeDragging()) mViewPager.beginFakeDrag();
         }
 
         @Override
         public void onTouchUp() {
-            // The user just stopped scrolling the carousel, so stop "fake dragging" the
-            // {@link ViewPager} if was doing so before.
-            if (mViewPager.isFakeDragging()) {
-                mViewPager.endFakeDrag();
-            }
+            // The user just stopped scrolling the carousel, so stop
+            // "fake dragging" the {@link ViewPager} if it was doing so
+            // before.
+            if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
         }
 
         @Override
         public void onScrollChanged(int l, int t, int oldl, int oldt) {
-            // The user is scrolling the carousel, so send the scroll deltas to the
-            // {@link ViewPager} so it can move in sync.
+            // The user is scrolling the carousel, so send the scroll
+            // deltas to the {@link ViewPager} so it can move in sync.
             if (mViewPager.isFakeDragging()) {
-                mViewPager.fakeDragBy(oldl-l);
+                mViewPager.fakeDragBy(oldl - l);
             }
         }
 
diff --git a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
index 821b441..13f5970 100644
--- a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
+++ b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
@@ -101,9 +101,7 @@
         final ImageView target = getTarget();
         if (target == null) return null;
 
-        OnClickListener clickListener = new PhotoClickListener(
+        return new PhotoClickListener(
                 context, contactData, bitmap, getCompressedImage(), expandPhotoOnClick);
-        target.setOnClickListener(clickListener);
-        return clickListener;
     }
 }
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 21a2c5b..38dcfee 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -21,7 +21,6 @@
 import com.android.contacts.detail.ContactDetailPhotoSetter;
 import com.android.contacts.util.PhoneCapabilityTester;
 
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.util.AttributeSet;
@@ -61,7 +60,6 @@
     private ImageView mPhotoView;
     private TextView mStatusView;
     private ImageView mStatusPhotoView;
-    private OnClickListener mPhotoClickListener;
     private final ContactDetailPhotoSetter mPhotoSetter = new ContactDetailPhotoSetter();
 
     private Listener mListener;
@@ -85,6 +83,9 @@
     private int mAllowedHorizontalScrollLength = Integer.MIN_VALUE;
     private int mAllowedVerticalScrollLength = Integer.MIN_VALUE;
 
+    /** Factor to scale scroll-amount sent to listeners. */
+    private float mScrollScaleFactor = 1.0f;
+
     private static final float MAX_ALPHA = 0.5f;
 
     /**
@@ -93,6 +94,7 @@
     public interface Listener {
         public void onTouchDown();
         public void onTouchUp();
+
         public void onScrollChanged(int l, int t, int oldl, int oldt);
         public void onTabSelected(int position);
     }
@@ -119,14 +121,13 @@
         mTabAndShadowContainer = findViewById(R.id.tab_and_shadow_container);
         mAboutTab = (CarouselTab) findViewById(R.id.tab_about);
         mAboutTab.setLabel(mContext.getString(R.string.contactDetailAbout));
+        mAboutTab.setTouchInterceptorListener(mAboutTabTouchInterceptListener);
 
         mTabDivider = findViewById(R.id.tab_divider);
 
         mUpdatesTab = (CarouselTab) findViewById(R.id.tab_update);
         mUpdatesTab.setLabel(mContext.getString(R.string.contactDetailUpdates));
-
-        mAboutTab.enableTouchInterceptor(mAboutTabTouchInterceptListener);
-        mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener);
+        mUpdatesTab.setTouchInterceptorListener(mUpdatesTabTouchInterceptListener);
 
         mShadow = findViewById(R.id.shadow);
 
@@ -138,6 +139,13 @@
         // TODO: This should be moved down to mUpdatesTab, so that it hosts its own controls
         mStatusView = (TextView) mUpdatesTab.findViewById(R.id.status);
         mStatusPhotoView = (ImageView) mUpdatesTab.findViewById(R.id.status_photo);
+
+        // Workaround for framework issue... it shouldn't be necessary to have a
+        // clickable object in the hierarchy, but if not the horizontal scroll
+        // behavior doesn't work. Note: the "About" tab doesn't need this
+        // because we set a real click-handler elsewhere.
+        mStatusView.setClickable(true);
+        mStatusPhotoView.setClickable(true);
     }
 
     @Override
@@ -150,13 +158,18 @@
         // from the total length of the tabs.
         mAllowedHorizontalScrollLength = tabWidth * TAB_COUNT - screenWidth;
 
+        // Scrolling by mAllowedHorizontalScrollLength causes listeners to
+        // scroll by the entire screen amount; compute the scale-factor
+        // necessary to make this so.
+        mScrollScaleFactor = screenWidth / mAllowedHorizontalScrollLength;
+
         int tabHeight = Math.round(screenWidth * mTabHeightScreenWidthFraction) + mTabShadowHeight;
         // Set the child {@link LinearLayout} to be TAB_COUNT * the computed tab width so that the
         // {@link LinearLayout}'s children (which are the tabs) will evenly split that width.
         if (getChildCount() > 0) {
             View child = getChildAt(0);
 
-            // add 1 dip of seperation between the tabs
+            // add 1 dip of separation between the tabs
             final int seperatorPixels =
                     (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
                     getResources().getDisplayMetrics()) + 0.5f);
@@ -184,23 +197,26 @@
         }
     }
 
-    private final OnClickListener mAboutTabTouchInterceptListener = new OnClickListener() {
-        @Override
-        public void onClick(View v) {
-            if (mCurrentTab == TAB_INDEX_ABOUT && mPhotoClickListener != null) {
-                mPhotoClickListener.onClick(v);
-            } else {
-                mListener.onTabSelected(TAB_INDEX_ABOUT);
-            }
-        }
-    };
+    /** When clicked, selects the corresponding tab. */
+    private class TabClickListener implements OnClickListener {
+        private final int mTab;
 
-    private final OnClickListener mUpdatesTabTouchInterceptListener = new OnClickListener() {
+        public TabClickListener(int tab) {
+            super();
+            mTab = tab;
+        }
+
         @Override
         public void onClick(View v) {
-            mListener.onTabSelected(TAB_INDEX_UPDATES);
+            mListener.onTabSelected(mTab);
         }
-    };
+    }
+
+    private final TabClickListener mAboutTabTouchInterceptListener =
+            new TabClickListener(TAB_INDEX_ABOUT);
+
+    private final TabClickListener mUpdatesTabTouchInterceptListener =
+            new TabClickListener(TAB_INDEX_UPDATES);
 
     /**
      * Does in "appear" animation to allow a seamless transition from
@@ -308,9 +324,18 @@
     }
 
     @Override
-    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
-        super.onScrollChanged(l, t, oldl, oldt);
-        mListener.onScrollChanged(l, t, oldl, oldt);
+    protected void onScrollChanged(int l, int t, int oldL, int oldT) {
+        super.onScrollChanged(l, t, oldL, oldT);
+
+        // Since we never completely scroll the about/updates tabs off-screen,
+        // the draggable range is less than the width of the carousel. Our
+        // listeners don't care about this... if we scroll 75% percent of our
+        // draggable range, they want to scroll 75% of the entire carousel
+        // width, not the same number of pixels that we scrolled.
+        int scaledL = (int) (l * mScrollScaleFactor);
+        int oldScaledL = (int) (oldL * mScrollScaleFactor);
+        mListener.onScrollChanged(scaledL, t, oldScaledL, oldT);
+
         mLastScrollPosition = l;
         updateAlphaLayers();
     }
@@ -388,20 +413,24 @@
      * Updates the tab selection.
      */
     public void setCurrentTab(int position) {
+        final CarouselTab selected, deselected;
+
         switch (position) {
             case TAB_INDEX_ABOUT:
-                mAboutTab.showSelectedState();
-                mUpdatesTab.showDeselectedState();
-                mUpdatesTab.enableTouchInterceptor(mUpdatesTabTouchInterceptListener);
+                selected = mAboutTab;
+                deselected = mUpdatesTab;
                 break;
             case TAB_INDEX_UPDATES:
-                mUpdatesTab.showSelectedState();
-                mUpdatesTab.disableTouchInterceptor();
-                mAboutTab.showDeselectedState();
+                selected = mUpdatesTab;
+                deselected = mAboutTab;
                 break;
             default:
                 throw new IllegalStateException("Invalid tab position " + position);
         }
+        selected.showSelectedState();
+        selected.disableTouchInterceptor();
+        deselected.showDeselectedState();
+        deselected.enableTouchInterceptor();
         mCurrentTab = position;
     }
 
@@ -415,8 +444,17 @@
         // TODO: Move this into the {@link CarouselTab} class when the updates
         // fragment code is more finalized.
         final boolean expandOnClick = !PhoneCapabilityTester.isUsingTwoPanes(mContext);
-        mPhotoClickListener = mPhotoSetter.setupContactPhotoForClick(
+        OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(
                 mContext, contactData, mPhotoView, expandOnClick);
+
+        if (expandOnClick || contactData.isWritableContact(mContext)) {
+            mPhotoView.setOnClickListener(listener);
+        } else {
+            // Work around framework issue... if we instead use
+            // setClickable(false), then we can't swipe horizontally.
+            mPhotoView.setOnClickListener(null);
+        }
+
         ContactDetailDisplayUtils.setSocialSnippet(
                 mContext, contactData, mStatusView, mStatusPhotoView);
     }
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index fd59674..1f3ce55 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -177,11 +177,18 @@
         }
     }
 
+
     @Override
-    public void enableTouchInterceptor(OnClickListener clickListener) {
+    public void setTouchInterceptorListener(OnClickListener clickListener) {
+        if (mTouchInterceptLayer != null) {
+            mTouchInterceptLayer.setOnClickListener(clickListener);
+        }
+    }
+
+    @Override
+    public void enableTouchInterceptor() {
         if (mTouchInterceptLayer != null) {
             mTouchInterceptLayer.setVisibility(View.VISIBLE);
-            mTouchInterceptLayer.setOnClickListener(clickListener);
         }
     }
 
diff --git a/src/com/android/contacts/detail/ViewOverlay.java b/src/com/android/contacts/detail/ViewOverlay.java
index 58428b8..6b5b02f 100644
--- a/src/com/android/contacts/detail/ViewOverlay.java
+++ b/src/com/android/contacts/detail/ViewOverlay.java
@@ -27,18 +27,25 @@
 public interface ViewOverlay {
 
     /**
+     * Sets the callback that will be invoked when the touch-interceptor tapped
+     * while enabled.
+     */
+    public void setTouchInterceptorListener(OnClickListener listener);
+
+    /**
      * Sets the alpha value on the alpha layer (if there is one).
      */
     public void setAlphaLayerValue(float alpha);
 
     /**
-     * Makes the touch intercept layer on this fragment visible (if there is one). Also adds a click
-     * listener which is called when there is a touch event on the layer.
+     * Enables the touch intercept layer on this fragment, so that it intercepts
+     * and handles touch events.
      */
-    public void enableTouchInterceptor(OnClickListener clickListener);
+    public void enableTouchInterceptor();
 
     /**
-     * Makes the touch intercept layer on this fragment gone (if there is one).
+     * Disables the touch intercept layer on this fragment; touch events are
+     * handled normally by the view hierarchy under the overlay.
      */
     public void disableTouchInterceptor();
 }
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index cbf766e..5f5a855 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -100,7 +100,7 @@
     private static final int TONE_RELATIVE_VOLUME = 80;
 
     /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
-    private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_MUSIC;
+    private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
 
     /**
      * View (usually FrameLayout) containing mDigits field. This can be null, in which mDigits
@@ -483,11 +483,7 @@
         synchronized (mToneGeneratorLock) {
             if (mToneGenerator == null) {
                 try {
-                    // we want the user to be able to control the volume of the dial tones
-                    // outside of a call, so we use the stream type that is also mapped to the
-                    // volume control keys for this activity
                     mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
-                    getActivity().setVolumeControlStream(DIAL_TONE_STREAM_TYPE);
                 } catch (RuntimeException e) {
                     Log.w(TAG, "Exception caught while creating local tone generator: " + e);
                     mToneGenerator = null;