diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java index 1f6c506d3a..6b0d7a3e87 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java @@ -26,6 +26,7 @@ import static android.view.MotionEvent.INVALID_POINTER_ID; import static com.android.launcher3.Utilities.EDGE_NAV_BAR; import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS; +import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED; import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; @@ -430,6 +431,6 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC @Override public boolean allowInterceptByParent() { - return !mPassedPilferInputSlop; + return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED); } } diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java index c49b8f2d14..fb420a272a 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java @@ -24,12 +24,15 @@ import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.Utilities.squaredHypot; +import static java.lang.Math.abs; + import android.content.Context; import android.graphics.PointF; -import android.view.GestureDetector; +import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BaseDraggingActivity; @@ -44,24 +47,36 @@ import com.android.systemui.shared.system.InputMonitorCompat; * Input consumer for handling events to pass to an {@code OverscrollPlugin}. */ public class OverscrollInputConsumer extends DelegateInputConsumer { - private static final String TAG = "OverscrollInputConsumer"; + private static final boolean DEBUG_LOGS_ENABLED = false; + private static void debugPrint(String log) { + if (DEBUG_LOGS_ENABLED) { + Log.v(TAG, log); + } + } private final PointF mDownPos = new PointF(); private final PointF mLastPos = new PointF(); private final PointF mStartDragPos = new PointF(); private final int mAngleThreshold; - private final float mFlingThresholdPx; + private final int mFlingDistanceThresholdPx; + private final int mFlingVelocityThresholdPx; private int mActivePointerId = -1; private boolean mPassedSlop = false; - + // True if we set ourselves as active, meaning we no longer pass events to the delegate. + private boolean mPassedActiveThreshold = false; + // When a gesture crosses this length, this recognizer will attempt to interpret touch events. private final float mSquaredSlop; + // When a gesture crosses this length, this recognizer will become the sole active recognizer. + private final float mSquaredActiveThreshold; + // When a gesture crosses this length, the overscroll view should be shown. + private final float mSquaredFinishThreshold; + private boolean mThisDownIsIgnored = false; private final GestureState mGestureState; @Nullable private final OverscrollPlugin mPlugin; - private final GestureDetector mGestureDetector; @Nullable private RecentsView mRecentsView; @@ -72,15 +87,24 @@ public class OverscrollInputConsumer extends DelegateInputConsumer { mAngleThreshold = context.getResources() .getInteger(R.integer.assistant_gesture_corner_deg_threshold); - mFlingThresholdPx = context.getResources() - .getDimension(R.dimen.gestures_overscroll_fling_threshold); + mFlingDistanceThresholdPx = (int) context.getResources() + .getDimension(R.dimen.gestures_overscroll_fling_threshold); + mFlingVelocityThresholdPx = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); mGestureState = gestureState; mPlugin = plugin; float slop = ViewConfiguration.get(context).getScaledTouchSlop(); mSquaredSlop = slop * slop; - mGestureDetector = new GestureDetector(context, new FlingGestureListener()); + + + float finishGestureThreshold = (int) context.getResources() + .getDimension(R.dimen.gestures_overscroll_finish_threshold); + mSquaredFinishThreshold = finishGestureThreshold * finishGestureThreshold; + + float activeThreshold = (int) context.getResources() + .getDimension(R.dimen.gestures_overscroll_active_threshold); + mSquaredActiveThreshold = activeThreshold * activeThreshold; } @Override @@ -90,12 +114,26 @@ public class OverscrollInputConsumer extends DelegateInputConsumer { @Override public void onMotionEvent(MotionEvent ev) { + if (mPlugin == null) { + return; + } + + debugPrint("got event, underlying activity is " + getUnderlyingActivity()); switch (ev.getActionMasked()) { case ACTION_DOWN: { + debugPrint("ACTION_DOWN"); mActivePointerId = ev.getPointerId(0); mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); - + if (mPlugin.blockOtherGestures()) { + debugPrint("mPlugin.blockOtherGestures(), becoming active on ACTION_DOWN"); + // Otherwise, if an appear gesture is performed when the Activity is visible, + // the Activity will dismiss its keyboard. + mPassedActiveThreshold = true; + mPassedSlop = true; + mStartDragPos.set(mLastPos.x, mLastPos.y); + setActive(ev); + } break; } case ACTION_POINTER_DOWN: { @@ -121,57 +159,61 @@ public class OverscrollInputConsumer extends DelegateInputConsumer { if (mState == STATE_DELEGATE_ACTIVE) { break; } + if (!mDelegate.allowInterceptByParent()) { mState = STATE_DELEGATE_ACTIVE; break; } + + // Update last touch position. int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { break; } mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); - if (!mPassedSlop) { - // Normal gesture, ensure we pass the slop before we start tracking the gesture - if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) - > mSquaredSlop) { - - mPassedSlop = true; - mStartDragPos.set(mLastPos.x, mLastPos.y); - if (isOverscrolled()) { - setActive(ev); - - if (mPlugin != null) { - mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity()); - } - } else { - mState = STATE_DELEGATE_ACTIVE; - } - } + float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y); + if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) { + mPassedSlop = true; + mStartDragPos.set(mLastPos.x, mLastPos.y); + mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED); } - if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled() - && mPlugin != null) { - mPlugin.onTouchTraveled(getDistancePx()); + boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled() + && (squaredDist > mSquaredActiveThreshold); + if (becomeActive) { + debugPrint("Past slop and past threshold, set active"); + mPassedActiveThreshold = true; + setActive(ev); + } + + if (mPassedActiveThreshold) { + debugPrint("ACTION_MOVE Relaying touch event"); + mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(), + (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx, + mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity()); } break; } case ACTION_CANCEL: case ACTION_UP: - if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) { - mPlugin.onTouchEnd(getDistancePx()); + debugPrint("ACTION_UP"); + if (mPassedActiveThreshold) { + debugPrint("ACTION_UP Relaying touch event"); + + mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(), + (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx, + mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity()); } + mPassedSlop = false; + mPassedActiveThreshold = false; mState = STATE_INACTIVE; break; } - if (mState != STATE_DELEGATE_ACTIVE) { - mGestureDetector.onTouchEvent(ev); - } - if (mState != STATE_ACTIVE) { mDelegate.onMotionEvent(ev); } @@ -192,15 +234,20 @@ public class OverscrollInputConsumer extends DelegateInputConsumer { maxIndex = 1; } - boolean atRightMostApp = (mRecentsView == null - || mRecentsView.getRunningTaskIndex() <= maxIndex); + boolean atRightMostApp = mRecentsView == null + || (mRecentsView.getRunningTaskIndex() <= maxIndex); // Check if the gesture is within our angle threshold of horizontal - float deltaY = Math.abs(mLastPos.y - mDownPos.y); - float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left - boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold; + float deltaY = abs(mLastPos.y - mDownPos.y); + float deltaX = mLastPos.x - mDownPos.x; - return atRightMostApp && angleInBounds; + boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, abs(deltaX))) < mAngleThreshold); + + boolean overscrollVisible = mPlugin.blockOtherGestures(); + boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0; + boolean gestureDirectionMatchesVisibility = overscrollVisible + || overscrollInvisibleAndLeftSwipe; + return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility; } private String getDeviceState() { @@ -219,35 +266,22 @@ public class OverscrollInputConsumer extends DelegateInputConsumer { return deviceState; } - private int getDistancePx() { - return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y); + private int getHorizontalDistancePx() { + return (int) (mLastPos.x - mDownPos.x); } - private String getUnderlyingActivity() { + private int getVerticalDistancePx() { + return (int) (mLastPos.y - mDownPos.y); + } + + private @NonNull String getUnderlyingActivity() { + // Overly defensive, got guidance on code review that something in the chain of + // `mGestureState.getRunningTask().topActivity` can be null and thus cause a null pointer + // exception to be thrown, but we aren't sure which part can be null. + if ((mGestureState == null) || (mGestureState.getRunningTask() == null) + || (mGestureState.getRunningTask().topActivity == null)) { + return ""; + } return mGestureState.getRunningTask().topActivity.flattenToString(); } - - private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (isValidAngle(velocityX, -velocityY) - && getDistancePx() >= mFlingThresholdPx - && mState != STATE_DELEGATE_ACTIVE) { - - if (mPlugin != null) { - mPlugin.onFling(-velocityX); - } - } - return true; - } - - private boolean isValidAngle(float deltaX, float deltaY) { - float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); - // normalize so that angle is measured clockwise from horizontal in the bottom right - // corner and counterclockwise from horizontal in the bottom left corner - - angle = angle > 90 ? 180 - angle : angle; - return (angle < mAngleThreshold); - } - } } diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index b06dc6b01d..85868049c5 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -79,6 +79,8 @@ 40dp + 80dp + 136dp 40dp diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java index f06e1a6afc..5adcc2ed70 100644 --- a/quickstep/src/com/android/quickstep/GestureState.java +++ b/quickstep/src/com/android/quickstep/GestureState.java @@ -107,6 +107,10 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL public static final int STATE_RECENTS_ANIMATION_ENDED = getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED"); + // Called when we create an overscroll window when swiping right to left on the most recent app + public static final int STATE_OVERSCROLL_WINDOW_CREATED = + getFlagForIndex("STATE_OVERSCROLL_WINDOW_CREATED"); + // Called when RecentsView stops scrolling and settles on a TaskView. public static final int STATE_RECENTS_SCROLLING_FINISHED = getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED"); diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 78d194bd98..69193399f7 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -110,6 +110,9 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag( "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture"); + public static final BooleanFlag ENABLE_QUICK_CAPTURE_WINDOW = getDebugFlag( + "ENABLE_QUICK_CAPTURE_WINDOW", false, "Use window to host quick capture"); + public static final BooleanFlag FORCE_LOCAL_OVERSCROLL_PLUGIN = getDebugFlag( "FORCE_LOCAL_OVERSCROLL_PLUGIN", false, "Use a launcher-provided OverscrollPlugin if available"); diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java index 28a9193bec..a434d078bc 100644 --- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java +++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java @@ -15,6 +15,8 @@ */ package com.android.systemui.plugins; +import android.view.MotionEvent; + import com.android.systemui.plugins.annotations.ProvidesInterface; /** @@ -28,7 +30,7 @@ import com.android.systemui.plugins.annotations.ProvidesInterface; public interface OverscrollPlugin extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL"; - int VERSION = 3; + int VERSION = 4; String DEVICE_STATE_LOCKED = "Locked"; String DEVICE_STATE_LAUNCHER = "Launcher"; @@ -41,33 +43,33 @@ public interface OverscrollPlugin extends Plugin { boolean isActive(); /** - * Called when a touch is down and has been recognized as an overscroll gesture. - * A call of this method will always result in `onTouchUp` being called, and possibly - * `onFling` as well. - * + * Called when a touch has been recognized as an overscroll gesture. + * @param horizontalDistancePx Horizontal distance from the last finger location to the finger + * location when it first touched the screen. + * @param verticalDistancePx Horizontal distance from the last finger location to the finger + * location when it first touched the screen. + * @param thresholdPx Minimum distance for gesture. + * @param flingDistanceThresholdPx Minimum distance for gesture by fling. + * @param flingVelocityThresholdPx Minimum velocity for gesture by fling. * @param deviceState String representing the current device state * @param underlyingActivity String representing the currently active Activity */ - void onTouchStart(String deviceState, String underlyingActivity); + void onTouchEvent(MotionEvent event, + int horizontalDistancePx, + int verticalDistancePx, + int thresholdPx, + int flingDistanceThresholdPx, + int flingVelocityThresholdPx, + String deviceState, + String underlyingActivity); /** - * Called when a touch that was previously recognized has moved. - * - * @param px distance between the position of touch on this update and the position of the - * touch when it was initially recognized. + * @return `true` if overscroll gesture handling should override all other gestures. */ - void onTouchTraveled(int px); + boolean blockOtherGestures(); /** - * Called when a touch that was previously recognized has ended. - * - * @param px distance between the position of touch on this update and the position of the - * touch when it was initially recognized. + * @return `true` if the overscroll gesture can pan the underlying app. */ - void onTouchEnd(int px); - - /** - * Called when the user starts Compose with a fling. `onTouchUp` will also be called. - */ - void onFling(float velocity); + boolean allowsUnderlyingActivityOverscroll(); }