Animate discovery bounce for all apps pull up interaction
b/30221381

> Will not support landscape case. If the user knows how to enable
rotation, it's very unlikely they don't know how to bring up apps drawer
> If unlocked using finger unlock immediately after screen is turned
off, bounce is not shown due to delay in ACTION_SCREEN_OFF broadcast

Change-Id: Ia8b7e572eaa4aeab8b1add1e5660fee3a63ba21c
diff --git a/res/anim/discovery_bounce.xml b/res/anim/discovery_bounce.xml
new file mode 100644
index 0000000..1f7d466
--- /dev/null
+++ b/res/anim/discovery_bounce.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+        android:ordering="sequentially"
+        android:startOffset="200">
+
+    <objectAnimator
+        android:propertyName="progress"
+        android:duration="250"
+        android:interpolator="@interpolator/disco_bounce_section1"
+        android:valueFrom="1f"
+        android:valueTo=".94f"
+        android:valueType="floatType"/>
+    <objectAnimator
+        android:propertyName="progress"
+        android:duration="216"
+        android:interpolator="@interpolator/disco_bounce_section2"
+        android:valueFrom=".94f"
+        android:valueTo="1.012f"
+        android:valueType="floatType"/>
+    <objectAnimator
+        android:propertyName="progress"
+        android:duration="234"
+        android:interpolator="@interpolator/disco_bounce_section3"
+        android:valueFrom="1.012f"
+        android:valueTo="1f"
+        android:valueType="floatType"/>
+</set>
diff --git a/res/interpolator/disco_bounce_section1.xml b/res/interpolator/disco_bounce_section1.xml
new file mode 100644
index 0000000..2156216
--- /dev/null
+++ b/res/interpolator/disco_bounce_section1.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:controlX1="0.9"
+                  android:controlY1="0"
+                  android:controlX2="0.5"
+                  android:controlY2="1"/>
diff --git a/res/interpolator/disco_bounce_section2.xml b/res/interpolator/disco_bounce_section2.xml
new file mode 100644
index 0000000..86cc789
--- /dev/null
+++ b/res/interpolator/disco_bounce_section2.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:controlX1="0.9"
+                  android:controlY1="0"
+                  android:controlX2="0.6"
+                  android:controlY2="1"/>
diff --git a/res/interpolator/disco_bounce_section3.xml b/res/interpolator/disco_bounce_section3.xml
new file mode 100644
index 0000000..1acef03
--- /dev/null
+++ b/res/interpolator/disco_bounce_section3.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2016 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
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:controlX1="0.1"
+                  android:controlY1="0"
+                  android:controlX2="0.7"
+                  android:controlY2="1"/>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7423e20..2cfe319 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -198,6 +198,7 @@
 
     static final String INTRO_SCREEN_DISMISSED = "launcher.intro_screen_dismissed";
     static final String FIRST_RUN_ACTIVITY_DISPLAYED = "launcher.first_run_activity_displayed";
+    static final String APPS_VIEW_SHOWN = "launcher.apps_view_shown";
 
     /** The different states that Launcher can be in. */
     enum State { NONE, WORKSPACE, WORKSPACE_SPRING_LOADED, APPS, APPS_SPRING_LOADED,
@@ -288,6 +289,7 @@
     private IconCache mIconCache;
     private ExtractedColors mExtractedColors;
     private LauncherAccessibilityDelegate mAccessibilityDelegate;
+    private boolean mIsResumeFromActionScreenOff;
     @Thunk boolean mUserPresent = true;
     private boolean mVisible = false;
     private boolean mHasFocus = false;
@@ -1079,6 +1081,10 @@
             InstallShortcutReceiver.disableAndFlushInstallQueue(this);
         }
 
+        if (shouldShowDiscoveryBounce()) {
+            mAllAppsController.showDiscoveryBounce();
+        }
+        mIsResumeFromActionScreenOff = false;
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onResume();
         }
@@ -1623,6 +1629,7 @@
                         mAppsView.reset();
                     }
                 }
+                mIsResumeFromActionScreenOff = true;
             } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
                 mUserPresent = true;
                 updateAutoAdvanceState();
@@ -3348,6 +3355,7 @@
      */
     public void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps,
             boolean focusSearchBar) {
+        markAppsViewShown();
         if (resetListToTop) {
             mAppsView.scrollToTop();
         }
@@ -4358,10 +4366,6 @@
                 !mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false);
     }
 
-    protected boolean hasRunFirstRunActivity() {
-        return mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false);
-    }
-
     public boolean showFirstRunActivity() {
         if (shouldRunFirstRunActivity() &&
                 hasFirstRunActivity()) {
@@ -4381,6 +4385,29 @@
         editor.apply();
     }
 
+    private void markAppsViewShown() {
+        if (mSharedPrefs.getBoolean(APPS_VIEW_SHOWN, false)) {
+            return;
+        }
+        mSharedPrefs.edit().putBoolean(APPS_VIEW_SHOWN, true).apply();
+    }
+
+    private boolean shouldShowDiscoveryBounce() {
+        if (mState != mState.WORKSPACE) {
+            return false;
+        }
+        if (mLauncherCallbacks != null && mLauncherCallbacks.shouldShowDiscoveryBounce()) {
+            return true;
+        }
+        if (!mIsResumeFromActionScreenOff) {
+            return false;
+        }
+        if (mSharedPrefs.getBoolean(APPS_VIEW_SHOWN, false)) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * To be overridden by subclasses to indicate that there is an in-activity full-screen intro
      * screen that must be displayed and dismissed.
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index e971631..2bbe0fb 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -110,4 +110,6 @@
      *                  but for implementation purposes is passed around as an object.
      */
     public void setLauncherSearchCallback(Object callbacks);
+
+    public boolean shouldShowDiscoveryBounce();
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 136308b..78280f7 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,6 +1,7 @@
 package com.android.launcher3.allapps;
 
 import android.animation.Animator;
+import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ArgbEvaluator;
@@ -13,15 +14,15 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.pageindicators.CaretDrawable;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.Workspace.Direction;
+import com.android.launcher3.pageindicators.CaretDrawable;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.TouchController;
 
@@ -64,15 +65,15 @@
     private final VerticalPullDetector mDetector;
     private final ArgbEvaluator mEvaluator;
 
-    // Animation in this class is controlled by a single variable {@link mShiftCurrent}.
-    // Visually, it represents top y coordinate of the all apps container. Using the
-    // {@link mShiftRange} as the denominator, this fraction value ranges in [0, 1].
-    //
-    // When {@link mShiftCurrent} is 0, all apps container is pulled up.
-    // When {@link mShiftCurrent} is {@link mShirtRange}, all apps container is pulled down.
+    // Animation in this class is controlled by a single variable {@link mProgress}.
+    // Visually, it represents top y coordinate of the all apps container if multiplied with
+    // {@link mShiftRange}.
+
+    // When {@link mProgress} is 0, all apps container is pulled up.
+    // When {@link mProgress} is 1, all apps container is pulled down.
     private float mShiftStart;      // [0, mShiftRange]
-    private float mShiftCurrent;    // [0, mShiftRange]
     private float mShiftRange;      // changes depending on the orientation
+    private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
 
     private static final float DEFAULT_SHIFT_RANGE = 10;
 
@@ -86,20 +87,26 @@
 
     private boolean mLightStatusBar;
 
-    public AllAppsTransitionController(Launcher launcher) {
-        mLauncher = launcher;
-        mDetector = new VerticalPullDetector(launcher);
+    // Used in discovery bounce animation to provide the transition without workspace changing.
+    private boolean mIsTranslateWithoutWorkspace = false;
+    private AnimatorSet mDiscoBounceAnimation;
+
+    public AllAppsTransitionController(Launcher l) {
+        mLauncher = l;
+        mDetector = new VerticalPullDetector(l);
         mDetector.setListener(this);
-        mShiftCurrent = mShiftRange = DEFAULT_SHIFT_RANGE;
-        mBezelSwipeUpHeight = launcher.getResources().getDimensionPixelSize(
+        mShiftRange = DEFAULT_SHIFT_RANGE;
+        mProgress = 1f;
+        mBezelSwipeUpHeight = l.getResources().getDimensionPixelSize(
                 R.dimen.all_apps_bezel_swipe_height);
 
-        mCaretAnimationDuration = launcher.getResources().getInteger(
+        mCaretAnimationDuration = l.getResources().getInteger(
                 R.integer.config_caretAnimationDuration);
-        mCaretInterpolator = AnimationUtils.loadInterpolator(launcher,
+        mCaretInterpolator = AnimationUtils.loadInterpolator(l,
                 R.interpolator.caret_animation_interpolator);
+
         mEvaluator = new ArgbEvaluator();
-        mAllAppsBackgroundColor = launcher.getColor(R.color.all_apps_container_color);
+        mAllAppsBackgroundColor = l.getColor(R.color.all_apps_container_color);
     }
 
     @Override
@@ -175,11 +182,11 @@
     }
 
     private boolean isInDisallowRecatchTopZone() {
-        return mShiftCurrent / mShiftRange < RECATCH_REJECTION_FRACTION;
+        return mProgress < RECATCH_REJECTION_FRACTION;
     }
 
     private boolean isInDisallowRecatchBottomZone() {
-        return mShiftCurrent / mShiftRange > 1 - RECATCH_REJECTION_FRACTION;
+        return mProgress > 1 - RECATCH_REJECTION_FRACTION;
     }
 
     @Override
@@ -195,8 +202,8 @@
         if (mAppsView == null) {
             return false;   // early termination.
         }
-        float progress = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
-        setProgress(progress);
+        float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
+        setProgress(shift / mShiftRange);
         return true;
     }
 
@@ -271,13 +278,11 @@
                 mAppsView.getRevealView().setVisibility(View.VISIBLE);
                 mAppsView.setRevealDrawableColor(mHotseatBackgroundColor);
             }
-        } else {
-            setProgress(mShiftCurrent);
         }
     }
 
-    private void updateLightStatusBar(float progress) {
-        boolean enable = progress <= mStatusBarHeight / 2;
+    private void updateLightStatusBar(float shift) {
+        boolean enable = shift <= mStatusBarHeight / 2;
         // Do not modify status bar on landscape as all apps is not full bleed.
         if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
             return;
@@ -300,38 +305,43 @@
     }
 
     /**
-     * @param progress y value of the border between hotseat and all apps
+     * @param progress value between 0 and 1
      */
     public void setProgress(float progress) {
-        updateLightStatusBar(progress);
-        mShiftCurrent = progress;
-        float alpha = calcAlphaAllApps(progress);
-        float workspaceHotseatAlpha = 1 - alpha;
+        mProgress = progress;
+        float shiftCurrent = progress * mShiftRange;
+
+        float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f);
+        float alpha = 1 - workspaceHotseatAlpha;
+
         float interpolation = mAccelInterpolator.getInterpolation(workspaceHotseatAlpha);
 
-        int color = (Integer) mEvaluator.evaluate(mDecelInterpolator.getInterpolation(alpha),
+        int color = (Integer) mEvaluator.evaluate(alpha,
                 mHotseatBackgroundColor, mAllAppsBackgroundColor);
         mAppsView.setRevealDrawableColor(color);
         mAppsView.getContentView().setAlpha(alpha);
-        mAppsView.setTranslationY(progress);
-        mWorkspace.setWorkspaceYTranslationAndAlpha(
-                PARALLAX_COEFFICIENT * (-mShiftRange + progress),
-                interpolation);
-        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-            mWorkspace.setHotseatTranslationAndAlpha(Direction.Y,
-                    PARALLAX_COEFFICIENT * (-mShiftRange + progress), interpolation);
+        mAppsView.setTranslationY(shiftCurrent);
+
+        if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+            mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y, -mShiftRange + shiftCurrent,
+                    interpolation);
         } else {
-            mWorkspace.setHotseatTranslationAndAlpha(Direction.Y,
-                    -mShiftRange + progress, interpolation);
+            mWorkspace.setHotseatTranslationAndAlpha(Workspace.Direction.Y,
+                    PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent),
+                    interpolation);
         }
+
+        if (mIsTranslateWithoutWorkspace) {
+            return;
+        }
+        mWorkspace.setWorkspaceYTranslationAndAlpha(
+                PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent),
+                interpolation);
+        updateLightStatusBar(shiftCurrent);
     }
 
     public float getProgress() {
-        return mShiftCurrent;
-    }
-
-    private float calcAlphaAllApps(float progress) {
-        return ((mShiftRange - progress) / mShiftRange);
+        return mProgress;
     }
 
     private void calculateDuration(float velocity, float disp) {
@@ -354,10 +364,8 @@
             mShiftStart = mAppsView.getTranslationY();
         }
         final float fromAllAppsTop = mAppsView.getTranslationY();
-        final float toAllAppsTop = 0;
-
         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
-                fromAllAppsTop, toAllAppsTop);
+                fromAllAppsTop / mShiftRange, 0f);
         driftAndAlpha.setDuration(mAnimationDuration);
         driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
         animationOut.play(driftAndAlpha);
@@ -387,6 +395,36 @@
         }
     }
 
+    public void showDiscoveryBounce() {
+        // cancel existing animation in case user locked and unlocked at a super human speed.
+        cancelDiscoveryAnimation();
+
+        // assumption is that this variable is always null
+        mDiscoBounceAnimation = (AnimatorSet) AnimatorInflater.loadAnimator(mLauncher,
+                R.anim.discovery_bounce);
+        mDiscoBounceAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+                mIsTranslateWithoutWorkspace = true;
+                preparePull(true);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                finishPullDown(false);
+                mDiscoBounceAnimation = null;
+                mIsTranslateWithoutWorkspace = false;
+            }
+        });
+        mDiscoBounceAnimation.setTarget(this);
+        mAppsView.post(new Runnable() {
+            @Override
+            public void run() {
+                mDiscoBounceAnimation.start();
+            }
+        });
+    }
+
     public void animateToWorkspace(AnimatorSet animationOut, long duration, boolean start) {
         if (animationOut == null) {
             return;
@@ -397,10 +435,9 @@
             mShiftStart = mAppsView.getTranslationY();
         }
         final float fromAllAppsTop = mAppsView.getTranslationY();
-        final float toAllAppsTop = mShiftRange;
 
         ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
-                fromAllAppsTop, toAllAppsTop);
+                fromAllAppsTop / mShiftRange, 1f);
         driftAndAlpha.setDuration(mAnimationDuration);
         driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator());
         animationOut.play(driftAndAlpha);
@@ -411,7 +448,6 @@
             @Override
             public void onAnimationCancel(Animator animation) {
                 canceled = true;
-                setProgress(mShiftCurrent);
             }
 
             @Override
@@ -442,7 +478,7 @@
         mHotseat.setBackgroundTransparent(false /* transparent */);
         mHotseat.setVisibility(View.VISIBLE);
         mAppsView.reset();
-        setProgress(mShiftRange);
+        setProgress(1f);
         if (animated) {
             animateCaret();
         } else {
@@ -453,10 +489,18 @@
 
     private void cancelAnimation() {
         if (mCurrentAnimation != null) {
-            mCurrentAnimation.setDuration(0);
             mCurrentAnimation.cancel();
             mCurrentAnimation = null;
         }
+        cancelDiscoveryAnimation();
+    }
+
+    public void cancelDiscoveryAnimation() {
+        if (mDiscoBounceAnimation == null) {
+            return;
+        }
+        mDiscoBounceAnimation.cancel();
+        mDiscoBounceAnimation = null;
     }
 
     private void cleanUpAnimation() {
@@ -490,13 +534,14 @@
 
     @Override
     public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                               int oldLeft, int oldTop, int oldRight, int oldBottom) {
-        float prevShiftRatio = mShiftCurrent / mShiftRange;
+            int oldLeft, int oldTop, int oldRight, int oldBottom) {
         if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
             mShiftRange = top;
         } else {
             mShiftRange = bottom;
         }
-        setProgress(mShiftRange * prevShiftRatio);
+        setProgress(mProgress);
     }
+
+
 }
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 5f8faab..28b6f3e 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -280,6 +280,10 @@
         int action = ev.getAction();
 
         if (action == MotionEvent.ACTION_DOWN) {
+            // Cancel discovery bounce animation when a user start interacting on anywhere on
+            // dray layer even if mAllAppsController is NOT the active controller.
+            // TODO: handle other input other than touch
+            mAllAppsController.cancelDiscoveryAnimation();
             if (handleTouchDown(ev, true)) {
                 return true;
             }
diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java
index c62ce0e..ae552d2 100644
--- a/src/com/android/launcher3/testing/LauncherExtension.java
+++ b/src/com/android/launcher3/testing/LauncherExtension.java
@@ -255,5 +255,10 @@
         @Override
         public void onDetachedFromWindow() {
         }
+
+        @Override
+        public boolean shouldShowDiscoveryBounce() {
+            return false;
+        }
     }
 }