Hint for phone and camera.

Bug: 15189049
Bug: 15126962
Change-Id: Ie28fc8202ace4af56542201d168572ef6ad78d19
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 60a5643..c68388e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -322,6 +322,13 @@
     <!-- Distance between notifications and header when they are considered to be colliding. -->
     <dimen name="header_notifications_collide_distance">24dp</dimen>
 
-    <!-- Move distance for the hint animations on the lockscreen (unlock, phone, camera)-->
+    <!-- Move distance for the unlock hint animation on the lockscreen -->
     <dimen name="hint_move_distance">75dp</dimen>
+
+    <!-- Move distance for the other hint animations on the lockscreen (phone, camera)-->
+    <dimen name="hint_move_distance_sideways">60dp</dimen>
+
+    <!-- The width of the region on the left/right edge of the screen for performing the camera/
+         phone hints. -->
+    <dimen name="edge_tap_area_width">48dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 373f11f..7ab010a 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -572,6 +572,12 @@
     <!-- Shows when people have pressed the unlock icon to explain how to unlock. [CHAR LIMIT=60] -->
     <string name="keyguard_unlock">Swipe up to unlock</string>
 
+    <!-- Shows when people have clicked at the left edge of the screen to explain how to open the phone. In right-to-left languages, this is the opposite direction. [CHAR LIMIT=60] -->
+    <string name="phone_hint">Swipe right for phone</string>
+
+    <!-- Shows when people have clicked at the right edge of the screen to explain how to open the phone. In right-to-left languages, this is the opposite direction. [CHAR LIMIT=60] -->
+    <string name="camera_hint">Swipe left for camera</string>
+
     <string name="bugreport_tile_extended" translatable="false">%s\n%s (%s)</string>
 
     <!-- Zen mode condition: no exit criteria. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BounceInterpolator.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BounceInterpolator.java
index 367d326..0fdc185 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BounceInterpolator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BounceInterpolator.java
@@ -27,6 +27,7 @@
 
     @Override
     public float getInterpolation(float t) {
+        t *= 11f / 10f;
         if (t < 4f / 11f) {
             return SCALE_FACTOR * t * t;
         } else if (t < 8f / 11f) {
@@ -36,8 +37,7 @@
             float t2 = t - 9f / 11f;
             return SCALE_FACTOR * t2 * t2 + 15f / 16f;
         } else {
-            float t2 = t - 21f / 22f;
-            return SCALE_FACTOR * t2 * t2 + 63f / 64f;
+            return 1;
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java
index 0e3e9e6..25bb41a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java
@@ -28,6 +28,7 @@
 import android.view.ViewPropertyAnimator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+
 import com.android.systemui.R;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 
@@ -40,6 +41,9 @@
 
     private static final float SWIPE_MAX_ICON_SCALE_AMOUNT = 2.0f;
     private static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.7f;
+    private static final long HINT_PHASE1_DURATION = 250;
+    private static final long HINT_PHASE2_DURATION = 450;
+
     private final Context mContext;
 
     private FlingAnimationUtils mFlingAnimationUtils;
@@ -54,11 +58,13 @@
     private int mTouchSlop;
     private int mMinTranslationAmount;
     private int mMinFlingVelocity;
+    private int mHintDistance;
     private PowerManager mPowerManager;
     private final View mLeftIcon;
     private final View mCenterIcon;
     private final View mRightIcon;
     private Interpolator mFastOutSlowIn;
+    private Interpolator mBounceInterpolator;
     private Animator mSwipeAnimator;
     private boolean mCallbackCalled;
 
@@ -81,9 +87,12 @@
         mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
         mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
                 R.dimen.keyguard_min_swipe_amount);
+        mHintDistance =
+                mContext.getResources().getDimensionPixelSize(R.dimen.hint_move_distance_sideways);
         mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
         mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext,
                 android.R.interpolator.fast_out_slow_in);
+        mBounceInterpolator = new BounceInterpolator();
     }
 
     public boolean onTouchEvent(MotionEvent event) {
@@ -168,6 +177,83 @@
         return false;
     }
 
+    public void startHintAnimation(boolean right, Runnable onFinishedListener) {
+        startHintAnimationPhase1(right, onFinishedListener);
+    }
+
+    /**
+     * Phase 1: Move everything sidewards.
+     */
+    private void startHintAnimationPhase1(boolean right, final Runnable onFinishedListener) {
+        float target = right ? -mHintDistance : mHintDistance;
+        startHintTranslationAnimations(target, HINT_PHASE1_DURATION, mFastOutSlowIn);
+        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mTranslation = (float) animation.getAnimatedValue();
+            }
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) {
+                    mSwipeAnimator = null;
+                    onFinishedListener.run();
+                } else {
+                    startUnlockHintAnimationPhase2(onFinishedListener);
+                }
+            }
+        });
+        animator.setInterpolator(mFastOutSlowIn);
+        animator.setDuration(HINT_PHASE1_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+    }
+
+    /**
+     * Phase 2: Move back.
+     */
+    private void startUnlockHintAnimationPhase2(final Runnable onFinishedListener) {
+        startHintTranslationAnimations(0f /* target */, HINT_PHASE2_DURATION, mBounceInterpolator);
+        ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, 0f);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mTranslation = (float) animation.getAnimatedValue();
+            }
+        });
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mSwipeAnimator = null;
+                onFinishedListener.run();
+            }
+        });
+        animator.setInterpolator(mBounceInterpolator);
+        animator.setDuration(HINT_PHASE2_DURATION);
+        animator.start();
+        mSwipeAnimator = animator;
+    }
+
+    private void startHintTranslationAnimations(float target, long duration,
+            Interpolator interpolator) {
+        ArrayList<View> targetViews = mCallback.getTranslationViews();
+        for (View targetView : targetViews) {
+            targetView.animate()
+                    .setDuration(duration)
+                    .setInterpolator(interpolator)
+                    .translationX(target);
+        }
+    }
+
     private void onUserActivity(long when) {
         mPowerManager.userActivity(when, false);
     }
@@ -180,7 +266,6 @@
         View targetView = mTranslation > 0 ? mLeftIcon : mRightIcon;
         targetView.animate().cancel();
         if (mSwipeAnimator != null) {
-            mSwipeAnimator.removeAllListeners();
             mSwipeAnimator.cancel();
             hideInactiveIcons(true);
         }
@@ -218,11 +303,18 @@
             }
         });
         animator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
             @Override
             public void onAnimationEnd(Animator animation) {
                 mSwipeAnimator = null;
                 mSwipingInProgress = false;
-                if (!snapBack && !mCallbackCalled) {
+                if (!snapBack && !mCallbackCalled && !mCancelled) {
 
                     // ensure that the callback is called eventually
                     mCallback.onAnimationToSideStarted(mTranslation < 0);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 7fc7cdb..252f153 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -422,7 +422,9 @@
         }
         // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
         // implementation.
-        if (!mIsExpanding && !mQsExpanded && mStatusBar.getBarState() != StatusBarState.SHADE) {
+        if ((!mIsExpanding || mHintAnimationRunning)
+                && !mQsExpanded
+                && mStatusBar.getBarState() != StatusBarState.SHADE) {
             mPageSwiper.onTouchEvent(event);
             if (mPageSwiper.isSwipingInProgress()) {
                 return true;
@@ -796,8 +798,7 @@
     protected void onTrackingStopped(boolean expand) {
         super.onTrackingStopped(expand);
         mOverExpansion = 0.0f;
-        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */,
-                true /* animate */);
+        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */);
         if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
                 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
             mPageSwiper.showAllIcons(true);
@@ -837,14 +838,36 @@
 
     @Override
     public void onAnimationToSideStarted(boolean rightPage) {
-        if (rightPage) {
-            mKeyguardBottomArea.launchCamera();
-        } else {
+        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
+        if (start) {
             mKeyguardBottomArea.launchPhone();
+        } else {
+            mKeyguardBottomArea.launchCamera();
         }
         mBlockTouches = true;
     }
 
+    @Override
+    protected void onEdgeClicked(boolean right) {
+        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
+                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
+            return;
+        }
+        mHintAnimationRunning = true;
+        mPageSwiper.startHintAnimation(right, new Runnable() {
+            @Override
+            public void run() {
+                mHintAnimationRunning = false;
+                mStatusBar.onHintFinished();
+            }
+        });
+        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
+        if (start) {
+            mStatusBar.onPhoneHintStarted();
+        } else {
+            mStatusBar.onCameraHintStarted();
+        }
+    }
 
     @Override
     public float getPageWidth() {
@@ -858,7 +881,9 @@
 
     @Override
     public View getLeftIcon() {
-        return mKeyguardBottomArea.getPhoneImageView();
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getCameraImageView()
+                : mKeyguardBottomArea.getPhoneImageView();
     }
 
     @Override
@@ -868,6 +893,8 @@
 
     @Override
     public View getRightIcon() {
-        return mKeyguardBottomArea.getCameraImageView();
+        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
+                ? mKeyguardBottomArea.getPhoneImageView()
+                : mKeyguardBottomArea.getCameraImageView();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 4686933..ea8dd68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -50,6 +50,7 @@
     protected PhoneStatusBar mStatusBar;
     private float mPeekHeight;
     private float mHintDistance;
+    private int mEdgeTapAreaWidth;
     private float mInitialOffsetOnTouch;
     private float mExpandedFraction = 0;
     private float mExpandedHeight = 0;
@@ -59,6 +60,7 @@
     private boolean mTouchSlopExceeded;
     private int mTrackingPointer;
     protected int mTouchSlop;
+    protected boolean mHintAnimationRunning;
 
     private ValueAnimator mHeightAnimator;
     private ObjectAnimator mPeekAnimator;
@@ -111,6 +113,7 @@
         final ViewConfiguration configuration = ViewConfiguration.get(getContext());
         mTouchSlop = configuration.getScaledTouchSlop();
         mHintDistance = res.getDimension(R.dimen.hint_move_distance);
+        mEdgeTapAreaWidth = res.getDimensionPixelSize(R.dimen.edge_tap_area_width);
     }
 
     private void trackMovement(MotionEvent event) {
@@ -147,7 +150,6 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
-
                 mInitialTouchY = y;
                 mInitialTouchX = x;
                 mInitialOffsetOnTouch = mExpandedHeight;
@@ -156,10 +158,11 @@
                     initVelocityTracker();
                 }
                 trackMovement(event);
-                if (!waitForTouchSlop || mHeightAnimator != null) {
+                if (!waitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning)) {
                     if (mHeightAnimator != null) {
                         mHeightAnimator.cancel(); // end any outstanding animations
                     }
+                    mTouchSlopExceeded = true;
                     onTrackingStarted();
                 }
                 if (mExpandedHeight == 0) {
@@ -222,7 +225,7 @@
                     onTrackingStopped(expand);
                     fling(vel, expand);
                 } else {
-                    boolean expands = onEmptySpaceClick();
+                    boolean expands = onEmptySpaceClick(mInitialTouchX);
                     onTrackingStopped(expands);
                 }
                 if (mVelocityTracker != null) {
@@ -279,8 +282,9 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
-                if (mHeightAnimator != null) {
+                if (mHeightAnimator != null && !mHintAnimationRunning) {
                     mHeightAnimator.cancel(); // end any outstanding animations
+                    mTouchSlopExceeded = true;
                     return true;
                 }
                 mInitialTouchY = y;
@@ -305,6 +309,9 @@
                 trackMovement(event);
                 if (scrolledToBottom) {
                     if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) {
+                        if (mHeightAnimator != null) {
+                            mHeightAnimator.cancel();
+                        }
                         mInitialOffsetOnTouch = mExpandedHeight;
                         mInitialTouchY = y;
                         mInitialTouchX = x;
@@ -550,14 +557,22 @@
         }
         cancelPeek();
         onExpandingStarted();
-        startUnlockHintAnimationPhase1();
+        startUnlockHintAnimationPhase1(new Runnable() {
+            @Override
+            public void run() {
+                onExpandingFinished();
+                mStatusBar.onHintFinished();
+                mHintAnimationRunning = false;
+            }
+        });
         mStatusBar.onUnlockHintStarted();
+        mHintAnimationRunning = true;
     }
 
     /**
      * Phase 1: Move everything upwards.
      */
-    private void startUnlockHintAnimationPhase1() {
+    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
         float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
         ValueAnimator animator = createHeightAnimator(target);
         animator.setDuration(250);
@@ -574,10 +589,9 @@
             public void onAnimationEnd(Animator animation) {
                 if (mCancelled) {
                     mHeightAnimator = null;
-                    onExpandingFinished();
-                    mStatusBar.onUnlockHintFinished();
+                    onAnimationFinished.run();
                 } else {
-                    startUnlockHintAnimationPhase2();
+                    startUnlockHintAnimationPhase2(onAnimationFinished);
                 }
             }
         });
@@ -588,7 +602,7 @@
     /**
      * Phase 2: Bounce down.
      */
-    private void startUnlockHintAnimationPhase2() {
+    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
         ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
         animator.setDuration(450);
         animator.setInterpolator(mBounceInterpolator);
@@ -596,8 +610,7 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 mHeightAnimator = null;
-                onExpandingFinished();
-                mStatusBar.onUnlockHintFinished();
+                onAnimationFinished.run();
             }
         });
         animator.start();
@@ -620,7 +633,22 @@
      *
      * @return whether the panel will be expanded after the action performed by this method
      */
-    private boolean onEmptySpaceClick() {
+    private boolean onEmptySpaceClick(float x) {
+        if (mHintAnimationRunning) {
+            return true;
+        }
+        if (x < mEdgeTapAreaWidth) {
+            onEdgeClicked(false /* right */);
+            return true;
+        } else if (x > getWidth() - mEdgeTapAreaWidth) {
+            onEdgeClicked(true /* right */);
+            return true;
+        } else {
+            return onMiddleClicked();
+        }
+    }
+
+    private boolean onMiddleClicked() {
         switch (mStatusBar.getBarState()) {
             case StatusBarState.KEYGUARD:
                 startUnlockHintAnimation();
@@ -636,6 +664,8 @@
         }
     }
 
+    protected abstract void onEdgeClicked(boolean right);
+
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
                 + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index b1216e69..bb0785c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -2963,10 +2963,18 @@
         mKeyguardIndicationTextView.switchIndication(R.string.keyguard_unlock);
     }
 
-    public void onUnlockHintFinished() {
+    public void onHintFinished() {
         mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase);
     }
 
+    public void onCameraHintStarted() {
+        mKeyguardIndicationTextView.switchIndication(R.string.camera_hint);
+    }
+
+    public void onPhoneHintStarted() {
+        mKeyguardIndicationTextView.switchIndication(R.string.phone_hint);
+    }
+
     public void onTrackingStopped(boolean expand) {
         if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
             if (!expand && !mUnlockMethodCache.isMethodInsecure()) {