Merge "Import translations. DO NOT MERGE"
diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml
index 8f78811..13b8d9b 100644
--- a/res/layout/quickcontact_activity.xml
+++ b/res/layout/quickcontact_activity.xml
@@ -24,10 +24,14 @@
     android:focusableInTouchMode="true"
     android:descendantFocusability="afterDescendants" >
 
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/quickcontact_starting_empty_height"
+        android:id="@+id/transparent_view" />
+
     <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/quickcontact_starting_empty_height"
         android:background="@color/card_margin_color"
         android:id="@+id/toolbar_parent">
 
diff --git a/src/com/android/contacts/interactions/CalendarInteractionsLoader.java b/src/com/android/contacts/interactions/CalendarInteractionsLoader.java
index 8ab6c37..941698c 100644
--- a/src/com/android/contacts/interactions/CalendarInteractionsLoader.java
+++ b/src/com/android/contacts/interactions/CalendarInteractionsLoader.java
@@ -63,10 +63,8 @@
         }
         // Perform separate calendar queries for events in the past and future.
         Cursor cursor = getSharedEventsCursor(/* isFuture= */ true, mMaxFutureToRetrieve);
-        Log.v(TAG, "future cursor.count() " + cursor.getCount());
         List<ContactInteraction> interactions = getInteractionsFromEventsCursor(cursor);
         cursor = getSharedEventsCursor(/* isFuture= */ false, mMaxPastToRetrieve);
-        Log.v(TAG, "past cursor.count() " + cursor.getCount());
         List<ContactInteraction> interactions2 = getInteractionsFromEventsCursor(cursor);
 
         ArrayList<ContactInteraction> allInteractions = new ArrayList<ContactInteraction>(
@@ -74,6 +72,7 @@
         allInteractions.addAll(interactions);
         allInteractions.addAll(interactions2);
 
+        Log.v(TAG, "# ContactInteraction Loaded: " + allInteractions.size());
         return allInteractions;
     }
 
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 9533d75..b186038 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -16,6 +16,9 @@
 
 package com.android.contacts.quickcontact;
 
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
 import android.app.Activity;
@@ -80,7 +83,6 @@
 import com.android.contacts.common.model.dataitem.PhoneDataItem;
 import com.android.contacts.common.util.DataStatus;
 import com.android.contacts.detail.ContactDetailDisplayUtils;
-import com.android.contacts.common.util.UriUtils;
 import com.android.contacts.interactions.CalendarInteractionsLoader;
 import com.android.contacts.interactions.CallLogInteractionsLoader;
 import com.android.contacts.interactions.ContactDeletionInteraction;
@@ -97,7 +99,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -142,11 +143,15 @@
     private boolean mHasAlreadyBeenOpened;
 
     private ImageView mPhotoView;
+    private View mTransparentView;
     private ExpandingEntryCardView mCommunicationCard;
     private ExpandingEntryCardView mRecentCard;
     private MultiShrinkScroller mScroller;
     private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
     private AsyncTask<Void, Void, Void> mEntriesAndActionsTask;
+    private ColorDrawable mWindowShim;
+    private boolean mIsWaitingForOtherPieceOfExitAnimation;
+    private boolean mIsExitAnimationInProgress;
 
     private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3;
     private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
@@ -269,7 +274,11 @@
             = new MultiShrinkScrollerListener() {
         @Override
         public void onScrolledOffBottom() {
-            onBackPressed();
+            if (!mIsWaitingForOtherPieceOfExitAnimation) {
+                finish();
+                return;
+            }
+            mIsWaitingForOtherPieceOfExitAnimation = false;
         }
 
         @Override
@@ -281,6 +290,28 @@
         public void onExitFullscreen() {
             updateStatusBarColor();
         }
+
+        @Override
+        public void onStartScrollOffBottom() {
+            // Remove the window shim now that we are starting an Activity exit animation.
+            final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
+            final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowShim, "alpha", 0xFF, 0);
+            animator.addListener(mExitWindowShimAnimationListener);
+            animator.setDuration(duration).start();
+            mIsWaitingForOtherPieceOfExitAnimation = true;
+            mIsExitAnimationInProgress = true;
+        }
+    };
+
+    final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mIsWaitingForOtherPieceOfExitAnimation) {
+                finish();
+                return;
+            }
+            mIsWaitingForOtherPieceOfExitAnimation = false;
+        }
     };
 
     @Override
@@ -314,6 +345,15 @@
         mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
 
         mPhotoView = (ImageView) findViewById(R.id.photo);
+        mTransparentView = findViewById(R.id.transparent_view);
+        if (mScroller != null) {
+            mTransparentView.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mScroller.scrollOffBottom();
+                }
+            });
+        }
 
         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
         setActionBar(toolbar);
@@ -321,11 +361,11 @@
 
         mHasAlreadyBeenOpened = savedInstanceState != null;
 
-        final ColorDrawable windowShim = new ColorDrawable(SHIM_COLOR);
-        getWindow().setBackgroundDrawable(windowShim);
+        mWindowShim = new ColorDrawable(SHIM_COLOR);
+        getWindow().setBackgroundDrawable(mWindowShim);
         if (!mHasAlreadyBeenOpened) {
             final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
-            ObjectAnimator.ofInt(windowShim, "alpha", 0, 0xFF).setDuration(duration).start();
+            ObjectAnimator.ofInt(mWindowShim, "alpha", 0, 0xFF).setDuration(duration).start();
         }
 
         if (mScroller != null) {
@@ -878,8 +918,9 @@
     @Override
     public void onBackPressed() {
         if (mScroller != null) {
-            // TODO: implement exit animation if the scroller isn't already off the screen
-            finish();
+            if (!mIsExitAnimationInProgress) {
+                mScroller.scrollOffBottom();
+            }
         } else {
             super.onBackPressed();
         }
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
index 53179f5..b38f5cf 100644
--- a/src/com/android/contacts/widget/MultiShrinkScroller.java
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -6,19 +6,22 @@
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
+import android.hardware.display.DisplayManagerGlobal;
 import android.util.AttributeSet;
+import android.view.Display;
+import android.view.DisplayInfo;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewConfiguration;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.widget.EdgeEffect;
 import android.widget.ImageView;
@@ -46,6 +49,11 @@
      */
     private static final int PIXELS_PER_SECOND = 1000;
 
+    /**
+     * Length of the acceleration animations. This value was taken from ValueAnimator.java.
+     */
+    private static final int EXIT_FLING_ANIMATION_DURATION_MS = 300;
+
     private float[] mLastEventPosition = { 0, 0 };
     private VelocityTracker mVelocityTracker;
     private boolean mIsBeingDragged = false;
@@ -56,6 +64,7 @@
     private View mToolbar;
     private ImageView mPhotoView;
     private View mPhotoViewContainer;
+    private View mTransparentView;
     private MultiShrinkScrollerListener mListener;
     private int mHeaderTintColor;
     private int mMaximumHeaderHeight;
@@ -75,25 +84,31 @@
     public interface MultiShrinkScrollerListener {
         void onScrolledOffBottom();
 
+        void onStartScrollOffBottom();
+
         void onEnterFullscreen();
 
         void onExitFullscreen();
     }
 
-    private final AnimatorListener mHeaderExpandAnimationListener = new AnimatorListener() {
-        @Override
-        public void onAnimationStart(Animator animation) {}
-
+    private final AnimatorListener mHeaderExpandAnimationListener = new AnimatorListenerAdapter() {
         @Override
         public void onAnimationEnd(Animator animation) {
             mPhotoView.setClickable(true);
         }
+    };
 
+    private final AnimatorListener mSnapToBottomListener = new AnimatorListenerAdapter() {
         @Override
-        public void onAnimationCancel(Animator animation) {}
-
-        @Override
-        public void onAnimationRepeat(Animator animation) {}
+        public void onAnimationEnd(Animator animation) {
+            if (getScrollUntilOffBottom() > 0 && mListener != null) {
+                // Due to a rounding error, after the animation finished we haven't fully scrolled
+                // off the screen. Lie to the listener: tell it that we did scroll off the screen.
+                mListener.onScrolledOffBottom();
+                // No other messages need to be sent to the listener.
+                mListener = null;
+            }
+        }
     };
 
     /**
@@ -156,6 +171,7 @@
         mScrollViewChild = findViewById(R.id.card_container);
         mToolbar = findViewById(R.id.toolbar_parent);
         mPhotoViewContainer = findViewById(R.id.toolbar_parent);
+        mTransparentView = findViewById(R.id.transparent_view);
         mListener = listener;
 
         mPhotoView = (ImageView) findViewById(R.id.photo);
@@ -354,12 +370,24 @@
      */
     private void snapToBottom(int flingDelta) {
         if (-getScroll_ignoreOversizedHeader() - flingDelta > 0) {
-            mScroller.forceFinished(true);
-            ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
-                    getScroll() - getScrollUntilOffBottom());
-            translateAnimation.setRepeatCount(0);
-            translateAnimation.setInterpolator(new AccelerateInterpolator());
-            translateAnimation.start();
+            scrollOffBottom();
+        }
+    }
+
+    public void scrollOffBottom() {
+        final Interpolator interpolator = new AcceleratingFlingInterpolator(
+                EXIT_FLING_ANIMATION_DURATION_MS, getCurrentVelocity(),
+                getScrollUntilOffBottom());
+        mScroller.forceFinished(true);
+        ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
+                getScroll() - getScrollUntilOffBottom());
+        translateAnimation.setRepeatCount(0);
+        translateAnimation.setInterpolator(interpolator);
+        translateAnimation.setDuration(EXIT_FLING_ANIMATION_DURATION_MS);
+        translateAnimation.addListener(mSnapToBottomListener);
+        translateAnimation.start();
+        if (mListener != null) {
+            mListener.onStartScrollOffBottom();
         }
     }
 
@@ -413,7 +441,7 @@
     public int getScroll() {
         final LinearLayout.LayoutParams toolbarLayoutParams
                 = (LayoutParams) mToolbar.getLayoutParams();
-        return mTransparentStartHeight - toolbarLayoutParams.topMargin
+        return mTransparentStartHeight - getTransparentViewHeight()
                 + mIntermediateHeaderHeight - toolbarLayoutParams.height + mScrollView.getScrollY();
     }
 
@@ -425,7 +453,7 @@
     public int getScroll_ignoreOversizedHeader() {
         final LinearLayout.LayoutParams toolbarLayoutParams
                 = (LayoutParams) mToolbar.getLayoutParams();
-        return mTransparentStartHeight - toolbarLayoutParams.topMargin
+        return mTransparentStartHeight - getTransparentViewHeight()
                 + Math.max(mIntermediateHeaderHeight - toolbarLayoutParams.height, 0)
                 + mScrollView.getScrollY();
     }
@@ -434,9 +462,7 @@
      * Amount of transparent space above the header/toolbar.
      */
     public int getScrollNeededToBeFullScreen() {
-        final LinearLayout.LayoutParams toolbarLayoutParams
-                = (LayoutParams) mToolbar.getLayoutParams();
-        return toolbarLayoutParams.topMargin;
+        return getTransparentViewHeight();
     }
 
     /**
@@ -495,6 +521,9 @@
     }
 
     private float getCurrentVelocity() {
+        if (mVelocityTracker == null) {
+            return 0;
+        }
         mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
         return mVelocityTracker.getYVelocity();
     }
@@ -515,15 +544,24 @@
                 + Math.max(0, mScrollViewChild.getHeight() - getHeight() + mMinimumHeaderHeight);
     }
 
+    private int getTransparentViewHeight() {
+        return mTransparentView.getLayoutParams().height;
+    }
+
+    private void setTransparentViewHeight(int height) {
+        mTransparentView.getLayoutParams().height = height;
+        mTransparentView.setLayoutParams(mTransparentView.getLayoutParams());
+    }
+
     private void scrollUp(int delta) {
-        LinearLayout.LayoutParams toolbarLayoutParams = (LayoutParams) mToolbar.getLayoutParams();
-        if (toolbarLayoutParams.topMargin != 0) {
-            final int originalValue = toolbarLayoutParams.topMargin;
-            toolbarLayoutParams.topMargin -= delta;
-            toolbarLayoutParams.topMargin = Math.max(toolbarLayoutParams.topMargin, 0);
-            mToolbar.setLayoutParams(toolbarLayoutParams);
-            delta -= originalValue - toolbarLayoutParams.topMargin;
+        if (getTransparentViewHeight() != 0) {
+            final int originalValue = getTransparentViewHeight();
+            setTransparentViewHeight(getTransparentViewHeight() - delta);
+            setTransparentViewHeight(Math.max(0, getTransparentViewHeight()));
+            delta -= originalValue - getTransparentViewHeight();
         }
+        final LinearLayout.LayoutParams toolbarLayoutParams
+                = (LayoutParams) mToolbar.getLayoutParams();
         if (toolbarLayoutParams.height != mMinimumHeaderHeight) {
             final int originalValue = toolbarLayoutParams.height;
             toolbarLayoutParams.height -= delta;
@@ -535,12 +573,13 @@
     }
 
     private void scrollDown(int delta) {
-        LinearLayout.LayoutParams toolbarLayoutParams = (LayoutParams) mToolbar.getLayoutParams();
         if (mScrollView.getScrollY() > 0) {
             final int originalValue = mScrollView.getScrollY();
             mScrollView.scrollBy(0, delta);
             delta -= mScrollView.getScrollY() - originalValue;
         }
+        final LinearLayout.LayoutParams toolbarLayoutParams
+                = (LayoutParams) mToolbar.getLayoutParams();
         if (toolbarLayoutParams.height < mIntermediateHeaderHeight) {
             final int originalValue = toolbarLayoutParams.height;
             toolbarLayoutParams.height -= delta;
@@ -549,14 +588,17 @@
             mToolbar.setLayoutParams(toolbarLayoutParams);
             delta -= originalValue - toolbarLayoutParams.height;
         }
-        toolbarLayoutParams.topMargin -= delta;
-        mToolbar.setLayoutParams(toolbarLayoutParams);
+        setTransparentViewHeight(getTransparentViewHeight() - delta);
 
-        if (mListener != null && getScrollUntilOffBottom() <= 0) {
+        if (getScrollUntilOffBottom() <= 0) {
             post(new Runnable() {
                 @Override
                 public void run() {
-                    mListener.onScrolledOffBottom();
+                    if (mListener != null) {
+                        mListener.onScrolledOffBottom();
+                        // No other messages need to be sent to the listener.
+                        mListener = null;
+                    }
                 }
             });
         }
@@ -568,7 +610,7 @@
         final int toolbarHeight = mToolbar.getLayoutParams().height;
         // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
         mPhotoView.clearColorFilter();
-        if (toolbarHeight >= mIntermediateHeaderHeight) {
+        if (toolbarHeight >= mMaximumHeaderHeight) {
             mPhotoViewContainer.setElevation(0);
             return;
         }
@@ -576,14 +618,13 @@
             mColorFilter.setColor(mHeaderTintColor);
             mPhotoView.setColorFilter(mColorFilter);
             mPhotoViewContainer.setElevation(mToolbarElevation);
-        } else if (toolbarHeight <= mIntermediateHeaderHeight) {
-            mPhotoViewContainer.setElevation(0);
-            final int alphaBits = 0xff - 0xff * (toolbarHeight  - mMinimumHeaderHeight)
-                    / (mIntermediateHeaderHeight - mMinimumHeaderHeight);
-            final int color = alphaBits << 24 | (mHeaderTintColor & 0xffffff);
-            mColorFilter.setColor(color);
-            mPhotoView.setColorFilter(mColorFilter);
         }
+        mPhotoViewContainer.setElevation(0);
+        final int alphaBits = 0xff - 0xff * (toolbarHeight  - mMinimumHeaderHeight)
+                / (mMaximumHeaderHeight - mMinimumHeaderHeight);
+        final int color = alphaBits << 24 | (mHeaderTintColor & 0xffffff);
+        mColorFilter.setColor(color);
+        mPhotoView.setColorFilter(mColorFilter);
     }
 
     private void updateLastEventPosition(MotionEvent event) {
@@ -616,4 +657,53 @@
         mScroller.startScroll(0, getScroll(), 0, delta);
         invalidate();
     }
+
+    /**
+     * Interpolator that enforces a specific starting velocity. This is useful to avoid a
+     * discontinuity between dragging speed and flinging speed.
+     *
+     * Similar to a {@link android.view.animation.AccelerateInterpolator} in the sense that
+     * getInterpolation() is a quadratic function.
+     */
+    private static class AcceleratingFlingInterpolator implements Interpolator {
+
+        private final float mStartingSpeedPixelsPerFrame;
+        private final float mDurationMs;
+        private final int mPixelsDelta;
+        private final float mNumberFrames;
+
+        public AcceleratingFlingInterpolator(int durationMs, float startingSpeedPixelsPerSecond,
+                int pixelsDelta) {
+            mStartingSpeedPixelsPerFrame = startingSpeedPixelsPerSecond / getRefreshRate();
+            mDurationMs = durationMs;
+            mPixelsDelta = pixelsDelta;
+            mNumberFrames = mDurationMs / getFrameIntervalMs();
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            final float animationIntervalNumber = mNumberFrames * input;
+            final float linearDelta = (animationIntervalNumber * mStartingSpeedPixelsPerFrame)
+                    / mPixelsDelta;
+            // Add the results of a linear interpolator (with the initial speed) with the
+            // results of a AccelerateInterpolator.
+            if (mStartingSpeedPixelsPerFrame > 0) {
+                return Math.min(input * input + linearDelta, 1);
+            } else {
+                // Initial fling was in the wrong direction, make sure that the quadratic component
+                // grows faster in order to make up for this.
+                return Math.min(input * (input - linearDelta) + linearDelta, 1);
+            }
+        }
+
+        private float getRefreshRate() {
+            DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
+                    Display.DEFAULT_DISPLAY);
+            return di.refreshRate;
+        }
+
+        public long getFrameIntervalMs() {
+            return (long)(1000 / getRefreshRate());
+        }
+    }
 }