chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/partialcustomtab/PartialCustomTabHandleStrategy.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.customtabs.features.partialcustomtab;

import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;

import androidx.core.view.MotionEventCompat;

import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.customtabs.features.partialcustomtab.PartialCustomTabBottomSheetStrategy.HeightStatus;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabToolbar;

import java.util.function.BooleanSupplier;

/** Handling touch events for resizing the Window. */
class PartialCustomTabHandleStrategy extends GestureDetector.SimpleOnGestureListener
        implements CustomTabToolbar.HandleStrategy {
    /**
     * The base duration of the settling animation of the sheet. 218 ms is a spec for material
     * design (this is the minimum time a user is guaranteed to pay attention to something).
     */
    private static final long BASE_ANIMATION_DURATION_MS = 218;

    private static final int FLING_THRESHOLD_PX = 100;

    static final int FLING_VELOCITY_PIXELS_PER_MS = 1000;

    private final GestureDetector mGestureDetector;
    private float mLastPosY;
    private float mDeltaY;
    private boolean mSeenFirstMoveOrDown;
    private VelocityTracker mVelocityTracker;
    private Runnable mCloseHandler;

    private BooleanSupplier mIsFullHeight;
    private Supplier<Integer> mStatus;
    private DragEventCallback mDragEventCallback;

    /** Callback for drag events. */
    interface DragEventCallback {
        /**
         * Drag action gets started.
         * @param y Y position when the drag action starts.
         */
        void onDragStart(int y);

        /**
         * Drag action is in progress. Called for each move.
         * @param y Y position when the drag move happens.
         */
        void onDragMove(int y);

        /**
         * Drag action is finished.
         *
         * @param flingDistance fling distance when the drag action ends up in fling action. Zero if
         *     not.
         */
        boolean onDragEnd(int flingDistance);
    }

    public PartialCustomTabHandleStrategy(
            Context context,
            BooleanSupplier isFullHeight,
            Supplier<Integer> status,
            DragEventCallback dragEventCallback) {
        mIsFullHeight = isFullHeight;
        mStatus = status;
        mDragEventCallback = dragEventCallback;
        mGestureDetector = new GestureDetector(context, this, ThreadUtils.getUiThreadHandler());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return mIsFullHeight.getAsBoolean() ? false : mGestureDetector.onTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mStatus.get() == HeightStatus.TRANSITION) {
            return true;
        }
        // We will get events directly even when onInterceptTouchEvent() didn't return true,
        // because the sub View tree might not want this event, so check orientation and
        // multi-window flags here again.
        if (mIsFullHeight.getAsBoolean()) {
            return true;
        }

        float y = event.getRawY();
        switch (MotionEventCompat.getActionMasked(event)) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                if (!mSeenFirstMoveOrDown) {
                    mSeenFirstMoveOrDown = true;
                    mVelocityTracker.clear();
                    mLastPosY = y;
                    mDragEventCallback.onDragStart((int) y);
                } else {
                    mVelocityTracker.addMovement(event);
                    mDragEventCallback.onDragMove((int) y);
                }
                mDeltaY = y - mLastPosY;
                mLastPosY = y;
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mSeenFirstMoveOrDown) {
                    mVelocityTracker.computeCurrentVelocity(FLING_VELOCITY_PIXELS_PER_MS);
                    float v = Math.abs(mVelocityTracker.getYVelocity());
                    int flingDist = Math.abs(v) < FLING_THRESHOLD_PX ? 0 : getFlingDistance(v);
                    int direction = (int) Math.signum(mDeltaY);
                    if (!mDragEventCallback.onDragEnd((int) (flingDist * direction))) {
                        mCloseHandler.run();
                    }
                    mSeenFirstMoveOrDown = false;
                }
                return true;
            default:
                return true;
        }
    }

    @Override
    public void setCloseClickHandler(Runnable handler) {
        mCloseHandler = handler;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        // Always intercept scroll events.
        return true;
    }

    /**
     * Gets the distance of a fling based on the velocity and the base animation time. This
     * formula assumes the deceleration curve is quadratic (t^2), hence the displacement formula
     * should be: displacement = initialVelocity * duration / 2.
     * @param velocity The velocity of the fling.
     * @return The distance the fling would cover.
     */
    private int getFlingDistance(float velocity) {
        // This includes conversion from seconds to ms.
        return (int) (velocity * BASE_ANIMATION_DURATION_MS / 2000f);
    }
}