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

Commit 1dd8b3f4 authored by Beverly's avatar Beverly Committed by Beverly Tai
Browse files

Only trigger (un)lock icon on longpress

Add a custom touch handler to the lock/unlock
icon affordance so it only triggers on long press.
It will no longer trigger on tap nor fling.

Fixes: 208431845
Fixes: 207551612
Test: manual, atest SystemUITests
Change-Id: I0b2561db55cc35a06a9981cdeb2e70922101c96c
Merged-In: I0b2561db55cc35a06a9981cdeb2e70922101c96c
parent 7f947fbf
Loading
Loading
Loading
Loading
+117 −97
Original line number Diff line number Diff line
@@ -35,13 +35,14 @@ import android.hardware.biometrics.SensorLocationInternal;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.media.AudioAttributes;
import android.os.Process;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.MathUtils;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -83,6 +84,7 @@ import javax.inject.Inject;
 */
@StatusBarComponent.StatusBarScope
public class LockIconViewController extends ViewController<LockIconView> implements Dumpable {
    private static final String TAG = "LockIconViewController";
    private static final float sDefaultDensity =
            (float) DisplayMetrics.DENSITY_DEVICE_STABLE / (float) DisplayMetrics.DENSITY_DEFAULT;
    private static final int sLockIconRadiusPx = (int) (sDefaultDensity * 36);
@@ -91,6 +93,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
                .build();
    private static final long LONG_PRESS_TIMEOUT = 150L; // milliseconds

    @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
    @NonNull private final KeyguardViewController mKeyguardViewController;
@@ -112,6 +115,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
    @Nullable private final Vibrator mVibrator;
    @Nullable private final AuthRippleController mAuthRippleController;

    // Tracks the velocity of a touch to help filter out the touches that move too fast.
    private VelocityTracker mVelocityTracker;
    // The ID of the pointer for which ACTION_DOWN has occurred. -1 means no pointer is active.
    private int mActivePointerId = -1;
    private VibrationEffect mTick;

    private boolean mIsDozing;
    private boolean mIsBouncerShowing;
    private boolean mRunningFPS;
@@ -122,6 +131,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
    private boolean mUserUnlockedWithBiometric;
    private Runnable mCancelDelayedUpdateVisibilityRunnable;
    private Runnable mOnGestureDetectedRunnable;
    private Runnable mLongPressCancelRunnable;

    private boolean mUdfpsSupported;
    private float mHeightPixels;
@@ -181,7 +191,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
        mView.setImageDrawable(mIcon);
        mUnlockedLabel = resources.getString(R.string.accessibility_unlock_button);
        mLockedLabel = resources.getString(R.string.accessibility_lock_icon);
        dumpManager.registerDumpable("LockIconViewController", this);
        dumpManager.registerDumpable(TAG, this);
    }

    @Override
@@ -320,7 +330,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
                        getResources().getString(R.string.accessibility_enter_hint));
        public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(v, info);
            if (isClickable()) {
            if (isActionable()) {
                if (mShowLockIcon) {
                    info.addAction(mAccessibilityAuthenticateHint);
                } else if (mShowUnlockIcon) {
@@ -477,7 +487,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
                @Override
                public void onKeyguardVisibilityChanged(boolean showing) {
                    // reset mIsBouncerShowing state in case it was preemptively set
                    // onAffordanceClick
                    // onLongPress
                    mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
                    updateVisibility();
                }
@@ -571,74 +581,102 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
        }
    };

    private final GestureDetector mGestureDetector =
            new GestureDetector(new SimpleOnGestureListener() {
                public boolean onDown(MotionEvent e) {
                    if (!isClickable()) {
                        mDownDetected = false;
    /**
     * Handles the touch if it is within the lock icon view and {@link #isActionable()} is true.
     * Subsequently, will trigger {@link #onLongPress()} if a touch is continuously in the lock icon
     * area for {@link #LONG_PRESS_TIMEOUT} ms.
     *
     * Touch speed debouncing mimics logic from the velocity tracker in {@link UdfpsController}.
     */
    public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
        if (!onInterceptTouchEvent(event)) {
            cancelTouches();
            return false;
        }

                    // intercept all following touches until we see MotionEvent.ACTION_CANCEL UP or
                    // MotionEvent.ACTION_UP (see #onTouchEvent)
        mOnGestureDetectedRunnable = onGestureDetectedRunnable;
        switch(event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_HOVER_ENTER:
                if (mVibrator != null && !mDownDetected) {
                    if (mTick == null) {
                        mTick = UdfpsController.lowTick(getContext(), true,
                                LONG_PRESS_TIMEOUT);
                    }
                    mVibrator.vibrate(
                            Process.myUid(),
                            getContext().getOpPackageName(),
                                UdfpsController.EFFECT_CLICK,
                                "lockIcon-onDown",
                            mTick,
                            "lock-icon-tick",
                            VIBRATION_SONIFICATION_ATTRIBUTES);
                }

                    mDownDetected = true;
                    return true;
                }

                public void onLongPress(MotionEvent e) {
                    if (!wasClickableOnDownEvent()) {
                        return;
                // The pointer that causes ACTION_DOWN is always at index 0.
                // We need to persist its ID to track it during ACTION_MOVE that could include
                // data for many other pointers because of multi-touch support.
                mActivePointerId = event.getPointerId(0);
                if (mVelocityTracker == null) {
                    // To simplify the lifecycle of the velocity tracker, make sure it's never null
                    // after ACTION_DOWN, and always null after ACTION_CANCEL or ACTION_UP.
                    mVelocityTracker = VelocityTracker.obtain();
                } else {
                    // ACTION_UP or ACTION_CANCEL is not guaranteed to be called before a new
                    // ACTION_DOWN, in that case we should just reuse the old instance.
                    mVelocityTracker.clear();
                }
                mVelocityTracker.addMovement(event);

                    if (onAffordanceClick() && mVibrator != null) {
                        // only vibrate if the click went through and wasn't intercepted by falsing
                        mVibrator.vibrate(
                                Process.myUid(),
                                getContext().getOpPackageName(),
                                UdfpsController.EFFECT_CLICK,
                                "lockIcon-onLongPress",
                                VIBRATION_SONIFICATION_ATTRIBUTES);
                    }
                mDownDetected = true;
                mLongPressCancelRunnable = mExecutor.executeDelayed(
                        this::onLongPress, LONG_PRESS_TIMEOUT);
                break;
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_HOVER_MOVE:
                mVelocityTracker.addMovement(event);
                // Compute pointer velocity in pixels per second.
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocity = UdfpsController.computePointerSpeed(mVelocityTracker,
                        mActivePointerId);
                if (event.getClassification() != MotionEvent.CLASSIFICATION_DEEP_PRESS
                        && UdfpsController.exceedsVelocityThreshold(velocity)) {
                    Log.v(TAG, "lock icon long-press rescheduled due to "
                            + "high pointer velocity=" + velocity);
                    mLongPressCancelRunnable.run();
                    mLongPressCancelRunnable = mExecutor.executeDelayed(
                            this::onLongPress, LONG_PRESS_TIMEOUT);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_HOVER_EXIT:
                cancelTouches();
                break;
        }

                public boolean onSingleTapUp(MotionEvent e) {
                    if (!wasClickableOnDownEvent()) {
                        return false;
                    }
                    onAffordanceClick();
        return true;
    }

                public boolean onFling(MotionEvent e1, MotionEvent e2,
                        float velocityX, float velocityY) {
                    if (!wasClickableOnDownEvent()) {
    /**
     * Intercepts the touch if the onDown event and current event are within this lock icon view's
     * bounds.
     */
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!inLockIconArea(event) || !isActionable()) {
            return false;
        }
                    onAffordanceClick();

        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            return true;
        }

                private boolean wasClickableOnDownEvent() {
        return mDownDetected;
    }

                /**
                 * Whether we tried to launch the affordance.
                 *
                 * If falsing intercepts the click, returns false.
                 */
                private boolean onAffordanceClick() {
    private void onLongPress() {
        cancelTouches();
        if (mFalsingManager.isFalseTouch(LOCK_ICON)) {
                        return false;
            Log.v(TAG, "lock icon long-press rejected by the falsing manager.");
            return;
        }

        // pre-emptively set to true to hide view
@@ -651,49 +689,31 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
            mOnGestureDetectedRunnable.run();
        }
        mKeyguardViewController.showBouncer(/* scrim */ true);
                    return true;
    }
            });

    /**
     * Send touch events to this view and handles it if the touch is within this view and we are
     * in a 'clickable' state
     * @return whether to intercept the touch event
     */
    public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
        if (onInterceptTouchEvent(event)) {
            mOnGestureDetectedRunnable = onGestureDetectedRunnable;
            mGestureDetector.onTouchEvent(event);
            return true;
        }

    private void cancelTouches() {
        mDownDetected = false;
        return false;
        if (mLongPressCancelRunnable != null) {
            mLongPressCancelRunnable.run();
        }

    /**
     * Intercepts the touch if the onDown event and current event are within this lock icon view's
     * bounds.
     */
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (!inLockIconArea(event) || !isClickable()) {
            return false;
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }

        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            return true;
        if (mVibrator != null) {
            mVibrator.cancel();
        }

        return mDownDetected;
    }


    private boolean inLockIconArea(MotionEvent event) {
        return mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
                && (mView.getVisibility() == View.VISIBLE
                || (mAodFp != null && mAodFp.getVisibility() == View.VISIBLE));
    }

    private boolean isClickable() {
    private boolean isActionable() {
        return mUdfpsSupported || mShowUnlockIcon;
    }

+39 −17
Original line number Diff line number Diff line
@@ -103,6 +103,7 @@ import kotlin.Unit;
public class UdfpsController implements DozeReceiver {
    private static final String TAG = "UdfpsController";
    private static final long AOD_INTERRUPT_TIMEOUT_MILLIS = 1000;
    private static final long DEFAULT_VIBRATION_DURATION = 1000; // milliseconds

    // Minimum required delay between consecutive touch logs in milliseconds.
    private static final long MIN_TOUCH_LOG_INTERVAL = 50;
@@ -164,8 +165,7 @@ public class UdfpsController implements DozeReceiver {
    private boolean mAttemptedToDismissKeyguard;
    private Set<Callback> mCallbacks = new HashSet<>();

    // by default, use low tick
    private int mPrimitiveTick = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
    private static final int DEFAULT_TICK = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
    private final VibrationEffect mTick;

    @VisibleForTesting
@@ -327,12 +327,23 @@ public class UdfpsController implements DozeReceiver {
        }
    }

    private static float computePointerSpeed(@NonNull VelocityTracker tracker, int pointerId) {
    /**
     * Calculate the pointer speed given a velocity tracker and the pointer id.
     * This assumes that the velocity tracker has already been passed all relevant motion events.
     */
    public static float computePointerSpeed(@NonNull VelocityTracker tracker, int pointerId) {
        final float vx = tracker.getXVelocity(pointerId);
        final float vy = tracker.getYVelocity(pointerId);
        return (float) Math.sqrt(Math.pow(vx, 2.0) + Math.pow(vy, 2.0));
    }

    /**
     * Whether the velocity exceeds the acceptable UDFPS debouncing threshold.
     */
    public static boolean exceedsVelocityThreshold(float velocity) {
        return velocity > 750f;
    }

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
@@ -467,7 +478,7 @@ public class UdfpsController implements DozeReceiver {
                        final float v = computePointerSpeed(mVelocityTracker, mActivePointerId);
                        final float minor = event.getTouchMinor(idx);
                        final float major = event.getTouchMajor(idx);
                        final boolean exceedsVelocityThreshold = v > 750f;
                        final boolean exceedsVelocityThreshold = exceedsVelocityThreshold(v);
                        final String touchInfo = String.format(
                                "minor: %.1f, major: %.1f, v: %.1f, exceedsVelocityThreshold: %b",
                                minor, major, v, exceedsVelocityThreshold);
@@ -575,7 +586,7 @@ public class UdfpsController implements DozeReceiver {
        mConfigurationController = configurationController;
        mSystemClock = systemClock;
        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
        mTick = lowTick();
        mTick = lowTick(context, false /* useShortRampup */, DEFAULT_VIBRATION_DURATION);

        mSensorProps = findFirstUdfps();
        // At least one UDFPS sensor exists
@@ -610,32 +621,43 @@ public class UdfpsController implements DozeReceiver {
        udfpsHapticsSimulator.setUdfpsController(this);
    }

    private VibrationEffect lowTick() {
        boolean useLowTickDefault = mContext.getResources()
    /**
     * Returns the continuous low tick effect that starts playing on the udfps finger-down event.
     */
    public static VibrationEffect lowTick(
            Context context,
            boolean useShortRampUp,
            long duration
    ) {
        boolean useLowTickDefault = context.getResources()
                .getBoolean(R.bool.config_udfpsUseLowTick);
        int primitiveTick = DEFAULT_TICK;
        if (Settings.Global.getFloat(
                mContext.getContentResolver(),
                context.getContentResolver(),
                "tick-low", useLowTickDefault ? 1 : 0) == 0) {
            mPrimitiveTick = VibrationEffect.Composition.PRIMITIVE_TICK;
            primitiveTick = VibrationEffect.Composition.PRIMITIVE_TICK;
        }
        float tickIntensity = Settings.Global.getFloat(
                mContext.getContentResolver(),
                context.getContentResolver(),
                "tick-intensity",
                mContext.getResources().getFloat(R.dimen.config_udfpsTickIntensity));
                context.getResources().getFloat(R.dimen.config_udfpsTickIntensity));
        int tickDelay = Settings.Global.getInt(
                mContext.getContentResolver(),
                context.getContentResolver(),
                "tick-delay",
                mContext.getResources().getInteger(R.integer.config_udfpsTickDelay));
                context.getResources().getInteger(R.integer.config_udfpsTickDelay));

        VibrationEffect.Composition composition = VibrationEffect.startComposition();
        composition.addPrimitive(mPrimitiveTick, tickIntensity, 0);
        int primitives = 1000 / tickDelay;
        composition.addPrimitive(primitiveTick, tickIntensity, 0);
        int primitives = (int) (duration / tickDelay);
        float[] rampUp = new float[]{.48f, .58f, .69f, .83f};
        if (useShortRampUp) {
            rampUp = new float[]{.5f, .7f};
        }
        for (int i = 0; i < rampUp.length; i++) {
            composition.addPrimitive(mPrimitiveTick, tickIntensity * rampUp[i], tickDelay);
            composition.addPrimitive(primitiveTick, tickIntensity * rampUp[i], tickDelay);
        }
        for (int i = rampUp.length; i < primitives; i++) {
            composition.addPrimitive(mPrimitiveTick, tickIntensity, tickDelay);
            composition.addPrimitive(primitiveTick, tickIntensity, tickDelay);
        }
        return composition.compose();
    }
+4 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.classifier;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DIAGONAL_HORIZONTAL_ANGLE_RANGE;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DIAGONAL_VERTICAL_ANGLE_RANGE;
import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
import static com.android.systemui.classifier.Classifier.LOCK_ICON;
import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;

import android.provider.DeviceConfig;
@@ -71,7 +72,9 @@ class DiagonalClassifier extends FalsingClassifier {
            return Result.passed(0);
        }

        if (interactionType == LEFT_AFFORDANCE || interactionType == RIGHT_AFFORDANCE) {
        if (interactionType == LEFT_AFFORDANCE
                || interactionType == RIGHT_AFFORDANCE
                || interactionType == LOCK_ICON) {
            return Result.passed(0);
        }