Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a3b03418 authored by Matthew Ng's avatar Matthew Ng
Browse files

Moved assistant gesture to corners

The corners will be separated with quick switch by detecting at the slop
of the angle from touch down to that position. If over 30 deg then
assistant will be tracked otherwise quick switch while swipe up will not
be tracked at all.

Test: manual
Bug: 112934365
Change-Id: I6a3aeb1509d9706696a30ef1fba3ce7e3e5ec07c
parent da608461
Loading
Loading
Loading
Loading
+125 −94
Original line number Diff line number Diff line
@@ -22,59 +22,70 @@ import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import com.android.launcher3.anim.Interpolators;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.system.NavigationBarCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.launcher3.R;
import com.android.systemui.shared.system.NavigationBarCompat;

/**
 * Touch consumer for handling events to launch assistant from launcher
 */
public class AssistantTouchConsumer implements InputConsumer {
    private static final String TAG = "AssistantTouchConsumer";
    private static final long RETRACT_ANIMATION_DURATION_MS = 300;

    /* The assistant touch consume competes with quick switch InputConsumer gesture. The delegate
     * can be chosen to run if the angle passing the slop is lower than the threshold angle. When
     * this occurs, the state changes to {@link #STATE_DELEGATE_ACTIVE} where the next incoming
     * motion events are handled by the delegate instead of the assistant touch consumer. If the
     * angle is higher than the threshold, the state will change to {@link #STATE_ASSISTANT_ACTIVE}.
     */
    private static final int STATE_INACTIVE = 0;
    private static final int STATE_ASSISTANT_ACTIVE = 1;
    private static final int STATE_DELEGATE_ACTIVE = 2;

    private final PointF mDownPos = new PointF();
    private final PointF mLastPos = new PointF();
    private int mActivePointerId = -1;

    private final int mDisplayRotation;
    private final Rect mStableInsets = new Rect();
    private final PointF mStartDragPos = new PointF();

    private final float mDragSlop;
    private final float mTouchSlop;
    private final float mThreshold;

    private float mStartDisplacement;
    private boolean mPassedDragSlop;
    private boolean mPassedTouchSlop;
    private long mPassedTouchSlopTime;
    private int mActivePointerId = -1;
    private boolean mPassedSlop;
    private boolean mLaunchedAssistant;
    private float mDistance;
    private float mTimeFraction;
    private long mDragTime;
    private float mLastProgress;
    private int mState;

    private final float mDistThreshold;
    private final long mTimeThreshold;
    private final int mAngleThreshold;
    private final float mSlop;
    private final MotionPauseDetector mMotionPauseDetector;
    private final ISystemUiProxy mSysUiProxy;
    private final InputConsumer mConsumerDelegate;

    public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy) {
    public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy,
            InputConsumer delegate) {
        final Resources res = context.getResources();
        mSysUiProxy = systemUiProxy;

        mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx();
        mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx();
        mThreshold = context.getResources().getDimension(R.dimen.gestures_assistant_threshold);

        Display display = context.getSystemService(WindowManager.class).getDefaultDisplay();
        mDisplayRotation = display.getRotation();
        WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
        mConsumerDelegate = delegate;
        mMotionPauseDetector = new MotionPauseDetector(context);
        mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold);
        mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold);
        mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
        mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx();
        mState = STATE_INACTIVE;
    }

    @Override
@@ -82,15 +93,29 @@ public class AssistantTouchConsumer implements InputConsumer {
        return TYPE_ASSISTANT;
    }

    @Override
    public boolean isActive() {
        return mState != STATE_INACTIVE;
    }

    @Override
    public void onMotionEvent(MotionEvent ev) {
        // TODO add logging

        switch (ev.getActionMasked()) {
            case ACTION_DOWN: {
                mActivePointerId = ev.getPointerId(0);
                mDownPos.set(ev.getX(), ev.getY());
                mLastPos.set(mDownPos);
                mLastProgress = -1;
                mTimeFraction = 0;

                // Detect when the gesture decelerates to start the assistant
                mMotionPauseDetector.setOnMotionPauseListener(isPaused -> {
                    if (isPaused && mState == STATE_ASSISTANT_ACTIVE) {
                        mTimeFraction = 1;
                        updateAssistantProgress();
                    }
                });
                break;
            }
            case ACTION_POINTER_UP: {
@@ -107,94 +132,100 @@ public class AssistantTouchConsumer implements InputConsumer {
                break;
            }
            case ACTION_MOVE: {
                if (mState == STATE_DELEGATE_ACTIVE) {
                    break;
                }
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == -1) {
                    break;
                }
                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
                float displacement = getDisplacement(ev);

                if (!mPassedDragSlop) {
                    // Normal gesture, ensure we pass the drag slop before we start tracking
                    // the gesture
                    if (Math.abs(displacement) > mDragSlop) {
                        mPassedDragSlop = true;
                        mStartDisplacement = displacement;
                        mPassedTouchSlopTime = SystemClock.uptimeMillis();
                    }
                }

                if (!mPassedTouchSlop) {
                    if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >=
                        mTouchSlop) {
                        mPassedTouchSlop = true;
                        if (!mPassedDragSlop) {
                            mPassedDragSlop = true;
                            mStartDisplacement = displacement;
                            mPassedTouchSlopTime = SystemClock.uptimeMillis();
                        }
                    }
                }

                if (mPassedDragSlop) {
                    // Move
                    float distance = mStartDisplacement - displacement;
                    if (distance >= 0) {
                        onAssistantProgress(distance / mThreshold);
                if (!mPassedSlop) {
                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
                    if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) {
                        mPassedSlop = true;
                        mStartDragPos.set(mLastPos.x, mLastPos.y);
                        mDragTime = SystemClock.uptimeMillis();

                        // Determine if angle is larger than threshold for assistant detection
                        float angle = (float) Math.toDegrees(
                                Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x));
                        angle = angle > 90 ? 180 - angle : angle;
                        if (angle > mAngleThreshold) {
                            mState = STATE_ASSISTANT_ACTIVE;

                            if (mConsumerDelegate != null) {
                                // Send cancel event
                                MotionEvent event = MotionEvent.obtain(ev);
                                event.setAction(MotionEvent.ACTION_CANCEL);
                                mConsumerDelegate.onMotionEvent(event);
                            }
                        } else {
                            mState = STATE_DELEGATE_ACTIVE;
                        }
                    }
                } else {
                    // Movement
                    mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
                            mLastPos.y - mStartDragPos.y);
                    mMotionPauseDetector.addPosition(mDistance, 0);
                    if (mDistance >= 0) {
                        final long diff = SystemClock.uptimeMillis() - mDragTime;
                        mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1);
                        updateAssistantProgress();
                    }
                }
                break;
            }
            case ACTION_CANCEL:
                break;
            case ACTION_UP: {
                if (ev.getEventTime() - mPassedTouchSlopTime < ViewConfiguration.getTapTimeout()) {
                    onAssistantProgress(1);
            case ACTION_UP:
                if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) {
                    ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0)
                            .setDuration(RETRACT_ANIMATION_DURATION_MS);
                    animator.addUpdateListener(valueAnimator -> {
                            float progress = (float) valueAnimator.getAnimatedValue();
                            try {
                                mSysUiProxy.onAssistantProgress(progress);
                            } catch (RemoteException e) {
                                Log.w(TAG, "Failed to send SysUI start/send assistant progress: "
                                        + progress, e);
                            }

                    });
                    animator.setInterpolator(Interpolators.DEACCEL_2);
                    animator.start();
                }
                mMotionPauseDetector.clear();
                break;
        }

        if (mState != STATE_ASSISTANT_ACTIVE && mConsumerDelegate != null) {
            mConsumerDelegate.onMotionEvent(ev);
        }
    }

    private void onAssistantProgress(float progress) {
        if (mLastProgress == progress) {
            return;
        }
    private void updateAssistantProgress() {
        if (!mLaunchedAssistant) {
            float progress = Math.min(mDistance * 1f / mDistThreshold, 1) * mTimeFraction;
            mLastProgress = progress;
            try {
            mSysUiProxy.onAssistantProgress(Math.max(0, Math.min(1, progress)));
            if (progress >= 1 && !mLaunchedAssistant) {
                mSysUiProxy.onAssistantProgress(progress);

                if (mDistance >= mDistThreshold && mTimeFraction >= 1) {
                    mSysUiProxy.startAssistant(new Bundle());
                    mLaunchedAssistant = true;
                }
            mLastProgress = progress;
            } catch (RemoteException e) {
            Log.w(TAG, "Failed to notify SysUI to start/send assistant progress: " + progress, e);
        }
                Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + progress, e);
            }

    private boolean isNavBarOnRight() {
        return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0;
    }

    private boolean isNavBarOnLeft() {
        return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0;
    }

    private float getDisplacement(MotionEvent ev) {
        float eventX = ev.getX();
        float eventY = ev.getY();
        float displacement = eventY - mDownPos.y;
        if (isNavBarOnRight()) {
            displacement = eventX - mDownPos.x;
        } else if (isNavBarOnLeft()) {
            displacement = mDownPos.x - eventX;
        }
        return displacement;
    }

    static boolean withinTouchRegion(Context context, float x) {
        return x > context.getResources().getDisplayMetrics().widthPixels
                - context.getResources().getDimension(R.dimen.gestures_assistant_width);
    static boolean withinTouchRegion(Context context, MotionEvent ev) {
        final Resources res = context.getResources();
        final int width = res.getDisplayMetrics().widthPixels;
        final int height = res.getDisplayMetrics().heightPixels;
        final int size = res.getDimensionPixelSize(R.dimen.gestures_assistant_size);
        return (ev.getX() > width - size || ev.getX() < size) && ev.getY() > height - size;
    }
}
+21 −16
Original line number Diff line number Diff line
@@ -313,22 +313,28 @@ public class TouchInteractionService extends Service {
            mSwipeSharedState.clearAllState();
        }

        final ActivityControlHelper activityControl =
                mOverviewComponentObserver.getActivityControlHelper();
        if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) {
            return InputConsumer.NO_OP;
        } else if (mAssistantAvailable && mOverviewInteractionState.isSwipeUpGestureEnabled()
                && FeatureFlags.ENABLE_ASSISTANT_GESTURE.get()
                && AssistantTouchConsumer.withinTouchRegion(this, event.getX())) {
            return new AssistantTouchConsumer(this, mRecentsModel.getSystemUiProxy());
        } else if (mSwipeSharedState.goingToLauncher ||
                mOverviewComponentObserver.getActivityControlHelper().isResumed()) {
            return OverviewInputConsumer.newInstance(
                    mOverviewComponentObserver.getActivityControlHelper(), false);
                && AssistantTouchConsumer.withinTouchRegion(this, event)) {
            return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed()
                            ? createOtherActivityInputConsumer(event, runningTaskInfo) : null);
        } else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) {
            return OverviewInputConsumer.newInstance(activityControl, false);
        } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
                mOverviewComponentObserver.getActivityControlHelper().isInLiveTileMode()) {
            return OverviewInputConsumer.newInstance(
                    mOverviewComponentObserver.getActivityControlHelper(), false);
                activityControl.isInLiveTileMode()) {
            return OverviewInputConsumer.newInstance(activityControl, false);
        } else {
            ActivityControlHelper activityControl =
            return createOtherActivityInputConsumer(event, runningTaskInfo);
        }
    }

    private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event,
            RunningTaskInfo runningTaskInfo) {
        final ActivityControlHelper activityControl =
                mOverviewComponentObserver.getActivityControlHelper();
        boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event);
        return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel,
@@ -336,7 +342,6 @@ public class TouchInteractionService extends Service {
                shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer,
                this::onConsumerInactive, mSwipeSharedState);
    }
    }

    /**
     * To be called by the consumer when it's no longer active.
+4 −0
Original line number Diff line number Diff line
@@ -26,4 +26,8 @@
         determines how many thumbnails will be fetched in the background. -->
    <integer name="recentsThumbnailCacheSize">3</integer>
    <integer name="recentsIconCacheSize">12</integer>

    <!-- Assistant Gesture -->
    <integer name="assistant_gesture_min_time_threshold">200</integer>
    <integer name="assistant_gesture_corner_deg_threshold">30</integer>
</resources>
+2 −2
Original line number Diff line number Diff line
@@ -66,6 +66,6 @@
    <dimen name="shelf_surface_offset">24dp</dimen>

    <!-- Assistant Gestures -->
    <dimen name="gestures_assistant_width">70dp</dimen>
    <dimen name="gestures_assistant_threshold">200dp</dimen>
    <dimen name="gestures_assistant_size">28dp</dimen>
    <dimen name="gestures_assistant_drag_threshold">70dp</dimen>
</resources>