Merge "Continue scaling down recents past final position in 0 button mode" into ub-launcher3-qt-dev
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index a38979d..332e0fa 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -84,6 +84,11 @@
             android:clearTaskOnLaunch="true"
             android:exported="false" />
 
+        <activity android:name="com.android.quickstep.LockScreenRecentsActivity"
+                  android:theme="@android:style/Theme.NoDisplay"
+                  android:showOnLockScreen="true"
+                  android:directBootAware="true" />
+
     </application>
 
 </manifest>
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
new file mode 100644
index 0000000..65f323c
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Empty activity to start a recents transition
+ */
+public class LockScreenRecentsActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        finish();
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index 6ba1bf5..14bdec5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -479,7 +479,7 @@
             if (isInValidSystemUiState) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
-                return new DeviceLockedInputConsumer(this);
+                return createDeviceLockedInputConsumer(mAM.getRunningTask(0));
             } else {
                 return InputConsumer.NO_OP;
             }
@@ -512,16 +512,15 @@
     }
 
     private InputConsumer newBaseConsumer(boolean useSharedState, MotionEvent event) {
-        if (mKM.isDeviceLocked()) {
-            // This handles apps launched in direct boot mode (e.g. dialer) as well as apps launched
-            // while device is locked even after exiting direct boot mode (e.g. camera).
-            return new DeviceLockedInputConsumer(this);
-        }
-
         final RunningTaskInfo runningTaskInfo = mAM.getRunningTask(0);
         if (!useSharedState) {
             mSwipeSharedState.clearAllState();
         }
+        if (mKM.isDeviceLocked()) {
+            // This handles apps launched in direct boot mode (e.g. dialer) as well as apps launched
+            // while device is locked even after exiting direct boot mode (e.g. camera).
+            return createDeviceLockedInputConsumer(runningTaskInfo);
+        }
 
         final ActivityControlHelper activityControl =
                 mOverviewComponentObserver.getActivityControlHelper();
@@ -559,6 +558,15 @@
                 mSwipeSharedState, mInputMonitorCompat, mSwipeTouchRegion);
     }
 
+    private InputConsumer createDeviceLockedInputConsumer(RunningTaskInfo taskInfo) {
+        if (mMode == Mode.NO_BUTTON && taskInfo != null) {
+            return new DeviceLockedInputConsumer(this, mSwipeSharedState, mInputMonitorCompat,
+                    mSwipeTouchRegion, taskInfo.taskId);
+        } else {
+            return InputConsumer.NO_OP;
+        }
+    }
+
     /**
      * To be called by the consumer when it's no longer active.
      */
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index d01b5ec..db2af59 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -15,26 +15,102 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_POINTER_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.graphics.Point;
 import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.quickstep.LockScreenRecentsActivity;
+import com.android.quickstep.MultiStateCallback;
+import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.RecentsAnimationListenerSet;
+import com.android.quickstep.util.SwipeAnimationTargetSet;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.InputMonitorCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 /**
  * A dummy input consumer used when the device is still locked, e.g. from secure camera.
  */
-public class DeviceLockedInputConsumer implements InputConsumer {
+public class DeviceLockedInputConsumer implements InputConsumer,
+        SwipeAnimationTargetSet.SwipeAnimationListener {
+
+    private static final float SCALE_DOWN = 0.75f;
+
+    private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
+    private static int getFlagForIndex(int index, String name) {
+        if (DEBUG_STATES) {
+            STATE_NAMES[index] = name;
+        }
+        return 1 << index;
+    }
+
+    private static final int STATE_TARGET_RECEIVED =
+            getFlagForIndex(0, "STATE_TARGET_RECEIVED");
+    private static final int STATE_HANDLER_INVALIDATED =
+            getFlagForIndex(1, "STATE_HANDLER_INVALIDATED");
 
     private final Context mContext;
     private final float mTouchSlopSquared;
-    private final PointF mTouchDown = new PointF();
+    private final SwipeSharedState mSwipeSharedState;
+    private final InputMonitorCompat mInputMonitorCompat;
 
-    public DeviceLockedInputConsumer(Context context) {
+    private final PointF mTouchDown = new PointF();
+    private final ClipAnimationHelper mClipAnimationHelper;
+    private final ClipAnimationHelper.TransformParams mTransformParams;
+    private final Point mDisplaySize;
+    private final MultiStateCallback mStateCallback;
+    private final RectF mSwipeTouchRegion;
+    public final int mRunningTaskId;
+
+    private VelocityTracker mVelocityTracker;
+    private float mProgress;
+
+    private boolean mThresholdCrossed = false;
+
+    private SwipeAnimationTargetSet mTargetSet;
+
+    public DeviceLockedInputConsumer(Context context, SwipeSharedState swipeSharedState,
+            InputMonitorCompat inputMonitorCompat, RectF swipeTouchRegion, int runningTaskId) {
         mContext = context;
         mTouchSlopSquared = squaredTouchSlop(context);
+        mSwipeSharedState = swipeSharedState;
+        mClipAnimationHelper = new ClipAnimationHelper(context);
+        mTransformParams = new ClipAnimationHelper.TransformParams();
+        mInputMonitorCompat = inputMonitorCompat;
+        mSwipeTouchRegion = swipeTouchRegion;
+        mRunningTaskId = runningTaskId;
+
+        // Do not use DeviceProfile as the user data might be locked
+        mDisplaySize = new Point();
+        context.getSystemService(WindowManager.class).getDefaultDisplay().getRealSize(mDisplaySize);
+
+        // Init states
+        mStateCallback = new MultiStateCallback(STATE_NAMES);
+        mStateCallback.addCallback(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
+                this::endRemoteAnimation);
+
+        mVelocityTracker = VelocityTracker.obtain();
     }
 
     @Override
@@ -44,17 +120,137 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            return;
+        }
+        mVelocityTracker.addMovement(ev);
+
         float x = ev.getX();
         float y = ev.getY();
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mTouchDown.set(x, y);
-        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
-            if (squaredHypot(x - mTouchDown.x, y - mTouchDown.y) > mTouchSlopSquared) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mTouchDown.set(x, y);
+                break;
+            case ACTION_POINTER_DOWN: {
+                if (!mThresholdCrossed) {
+                    // Cancel interaction in case of multi-touch interaction
+                    int ptrIdx = ev.getActionIndex();
+                    if (!mSwipeTouchRegion.contains(ev.getX(ptrIdx), ev.getY(ptrIdx))) {
+                        int action = ev.getAction();
+                        ev.setAction(ACTION_CANCEL);
+                        finishTouchTracking(ev);
+                        ev.setAction(action);
+                    }
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                if (!mThresholdCrossed) {
+                    if (squaredHypot(x - mTouchDown.x, y - mTouchDown.y) > mTouchSlopSquared) {
+                        startRecentsTransition();
+                    }
+                } else {
+                    float dy = Math.max(mTouchDown.y - y, 0);
+                    mProgress = dy / mDisplaySize.y;
+                    mTransformParams.setProgress(mProgress);
+                    if (mTargetSet != null) {
+                        mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
+                    }
+                }
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                finishTouchTracking(ev);
+                break;
+        }
+    }
+
+    /**
+     * Called when the gesture has ended. Does not correlate to the completion of the interaction as
+     * the animation can still be running.
+     */
+    private void finishTouchTracking(MotionEvent ev) {
+        mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+        if (mThresholdCrossed && ev.getAction() == ACTION_UP) {
+            mVelocityTracker.computeCurrentVelocity(1000,
+                    ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
+
+            float velocityY = mVelocityTracker.getYVelocity();
+            float flingThreshold = mContext.getResources()
+                    .getDimension(R.dimen.quickstep_fling_threshold_velocity);
+
+            boolean dismissTask;
+            if (Math.abs(velocityY) > flingThreshold) {
+                // Is fling
+                dismissTask = velocityY < 0;
+            } else {
+                dismissTask = mProgress >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
+            }
+            if (dismissTask) {
                 // For now, just start the home intent so user is prompted to unlock the device.
                 mContext.startActivity(new Intent(Intent.ACTION_MAIN)
                         .addCategory(Intent.CATEGORY_HOME)
                         .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
             }
         }
+        mVelocityTracker.recycle();
+        mVelocityTracker = null;
+    }
+
+    private void startRecentsTransition() {
+        mThresholdCrossed = true;
+        RecentsAnimationListenerSet newListenerSet =
+                mSwipeSharedState.newRecentsAnimationListenerSet();
+        newListenerSet.addListener(this);
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_DEFAULT)
+                .setComponent(new ComponentName(mContext, LockScreenRecentsActivity.class))
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        mInputMonitorCompat.pilferPointers();
+        BackgroundExecutor.get().submit(
+                () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+                        intent, null, newListenerSet, null, null));
+    }
+
+    @Override
+    public void onRecentsAnimationStart(SwipeAnimationTargetSet targetSet) {
+        mTargetSet = targetSet;
+
+        Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
+        RemoteAnimationTargetCompat targetCompat = targetSet.findTask(mRunningTaskId);
+        if (targetCompat != null) {
+            mClipAnimationHelper.updateSource(displaySize, targetCompat);
+        }
+
+        Utilities.scaleRectAboutCenter(displaySize, SCALE_DOWN);
+        displaySize.offsetTo(displaySize.left, 0);
+        mClipAnimationHelper.updateTargetRect(displaySize);
+        mClipAnimationHelper.applyTransform(mTargetSet, mTransformParams);
+
+        mStateCallback.setState(STATE_TARGET_RECEIVED);
+    }
+
+    @Override
+    public void onRecentsAnimationCanceled() {
+        mTargetSet = null;
+    }
+
+    private void endRemoteAnimation() {
+        if (mTargetSet != null) {
+            mTargetSet.finishController(
+                    false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
+        }
+    }
+
+    @Override
+    public void onConsumerAboutToBeSwitched() {
+        mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+    }
+
+    @Override
+    public boolean allowInterceptByParent() {
+        return !mThresholdCrossed;
     }
 }
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index ba4ea8b..1869188 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -44,7 +44,7 @@
         <FrameLayout
             android:id="@+id/proactive_suggest_container"
             android:layout_width="match_parent"
-            android:layout_height="48dp"
+            android:layout_height="wrap_content"
             android:gravity="center"
             android:visibility="gone"
             />
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index 1d62b43..8c59626 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -30,6 +30,9 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserManagerCompat;
@@ -52,6 +55,16 @@
     private final Launcher mLauncher;
     private final Animator mDiscoBounceAnimation;
 
+    private final StateListener mStateListener = new StateListener() {
+        @Override
+        public void onStateTransitionStart(LauncherState toState) {
+            handleClose(false);
+        }
+
+        @Override
+        public void onStateTransitionComplete(LauncherState finalState) {}
+    };
+
     public DiscoveryBounce(Launcher launcher, float delta) {
         super(launcher, null);
         mLauncher = launcher;
@@ -67,6 +80,7 @@
             }
         });
         mDiscoBounceAnimation.addListener(controller.getProgressAnimatorListener());
+        launcher.getStateManager().addStateListener(mStateListener);
     }
 
     @Override
@@ -105,6 +119,7 @@
             // Reset the all-apps progress to what ever it was previously.
             mLauncher.getAllAppsController().setProgress(mLauncher.getStateManager()
                     .getState().getVerticalProgress(mLauncher));
+            mLauncher.getStateManager().removeStateListener(mStateListener);
         }
     }