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

Commit acd240fb authored by Dave Mankoff's avatar Dave Mankoff Committed by Android (Google) Code Review
Browse files

Merge changes from topic "b111394067-new-falsing-manager" into qt-dev

* changes:
  Add ZigZagClassifier to the BrightLineFalsingManager.
  Add ProximityClassifier to the BrightLineFalsingManager
  Add DistanceClassifier to the BrightLineFalsingManager
  Add DiagonalClassifier to the BrightLineFalsingManager.
  Add TypeClassifier to the BrightLineFalsingManager.
  Add PointerCountClassifier to the BrightLineFalsingManager.
  Add base class for new falsing manager and classifiers.
parents fcbde524 89ad2468
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -16,9 +16,13 @@

package com.android.systemui.classifier;

import android.annotation.IntDef;
import android.hardware.SensorEvent;
import android.view.MotionEvent;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * An abstract class for classifiers for touch and sensor events.
 */
@@ -34,6 +38,21 @@ public abstract class Classifier {
    public static final int BOUNCER_UNLOCK = 8;
    public static final int PULSE_EXPAND = 9;

    @IntDef({
            QUICK_SETTINGS,
            NOTIFICATION_DISMISS,
            NOTIFICATION_DRAG_DOWN,
            NOTIFICATION_DOUBLE_TAP,
            UNLOCK,
            LEFT_AFFORDANCE,
            RIGHT_AFFORDANCE,
            GENERIC,
            BOUNCER_UNLOCK,
            PULSE_EXPAND
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface InteractionType {}

    /**
     * Contains all the information about touch events from which the classifier can query
     */
+328 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.classifier.brightline;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
import android.util.Log;
import android.view.MotionEvent;

import com.android.systemui.classifier.Classifier;
import com.android.systemui.plugins.FalsingManager;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * FalsingManager designed to make clear why a touch was rejected.
 */
public class BrightLineFalsingManager implements FalsingManager {

    static final boolean DEBUG = false;
    private static final String TAG = "FalsingManagerPlugin";

    private final SensorManager mSensorManager;
    private final FalsingDataProvider mDataProvider;
    private boolean mSessionStarted;

    private final ExecutorService mBackgroundExecutor = Executors.newSingleThreadExecutor();

    private final List<FalsingClassifier> mClassifiers;

    private SensorEventListener mSensorEventListener = new SensorEventListener() {
        @Override
        public synchronized void onSensorChanged(SensorEvent event) {
            onSensorEvent(event);
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
        }
    };

    BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, SensorManager sensorManager) {
        mDataProvider = falsingDataProvider;
        mSensorManager = sensorManager;
        mClassifiers = new ArrayList<>();
        DistanceClassifier distanceClassifier = new DistanceClassifier(mDataProvider);
        ProximityClassifier proximityClassifier = new ProximityClassifier(distanceClassifier,
                mDataProvider);
        mClassifiers.add(new PointerCountClassifier(mDataProvider));
        mClassifiers.add(new TypeClassifier(mDataProvider));
        mClassifiers.add(new DiagonalClassifier(mDataProvider));
        mClassifiers.add(distanceClassifier);
        mClassifiers.add(proximityClassifier);
        mClassifiers.add(new ZigZagClassifier(mDataProvider));
    }

    private void registerSensors() {
        Sensor s = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
        if (s != null) {
            // This can be expensive, and doesn't need to happen on the main thread.
            mBackgroundExecutor.submit(() -> {
                logDebug("registering sensor listener");
                mSensorManager.registerListener(
                        mSensorEventListener, s, SensorManager.SENSOR_DELAY_GAME);
            });
        }
    }


    private void unregisterSensors() {
        // This can be expensive, and doesn't need to happen on the main thread.
        mBackgroundExecutor.submit(() -> {
            logDebug("unregistering sensor listener");
            mSensorManager.unregisterListener(mSensorEventListener);
        });
    }

    private void sessionStart() {
        logDebug("Starting Session");
        mSessionStarted = true;
        registerSensors();
        mClassifiers.forEach(FalsingClassifier::onSessionStarted);
    }

    private void sessionEnd() {
        if (mSessionStarted) {
            logDebug("Ending Session");
            mSessionStarted = false;
            unregisterSensors();
            mDataProvider.onSessionEnd();
            mClassifiers.forEach(FalsingClassifier::onSessionEnded);
        }
    }

    private void updateInteractionType(@Classifier.InteractionType int type) {
        logDebug("InteractionType: " + type);
        mClassifiers.forEach((classifier) -> classifier.setInteractionType(type));
    }

    @Override
    public boolean isClassiferEnabled() {
        return true;
    }

    @Override
    public boolean isFalseTouch() {
        boolean r = mClassifiers.stream().anyMatch(falsingClassifier -> {
            boolean result = falsingClassifier.isFalseTouch();
            if (result) {
                logInfo(falsingClassifier.getClass().getName() + ": true");
            } else {
                logDebug(falsingClassifier.getClass().getName() + ": false");
            }
            return result;
        });

        logDebug("Is false touch? " + r);

        return r;
    }

    @Override
    public void onTouchEvent(MotionEvent motionEvent, int width, int height) {
        // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
        // make these calls.
        mDataProvider.onMotionEvent(motionEvent);
        mClassifiers.forEach((classifier) -> classifier.onTouchEvent(motionEvent));
    }

    private void onSensorEvent(SensorEvent sensorEvent) {
        // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
        // make these calls.
        mClassifiers.forEach((classifier) -> classifier.onSensorEvent(sensorEvent));
    }

    @Override
    public void onSucccessfulUnlock() {
    }

    @Override
    public void onNotificationActive() {
    }

    @Override
    public void setShowingAod(boolean showingAod) {
        if (showingAod) {
            sessionEnd();
        } else {
            sessionStart();
        }
    }

    @Override
    public void onNotificatonStartDraggingDown() {
        updateInteractionType(Classifier.NOTIFICATION_DRAG_DOWN);

    }

    @Override
    public boolean isUnlockingDisabled() {
        return false;
    }


    @Override
    public void onNotificatonStopDraggingDown() {
    }

    @Override
    public void setNotificationExpanded() {
    }

    @Override
    public void onQsDown() {
        updateInteractionType(Classifier.QUICK_SETTINGS);
    }

    @Override
    public void setQsExpanded(boolean b) {
    }

    @Override
    public boolean shouldEnforceBouncer() {
        return false;
    }

    @Override
    public void onTrackingStarted(boolean secure) {
        updateInteractionType(secure ? Classifier.BOUNCER_UNLOCK : Classifier.UNLOCK);
    }

    @Override
    public void onTrackingStopped() {
    }

    @Override
    public void onLeftAffordanceOn() {
    }

    @Override
    public void onCameraOn() {
    }

    @Override
    public void onAffordanceSwipingStarted(boolean rightCorner) {
        updateInteractionType(
                rightCorner ? Classifier.RIGHT_AFFORDANCE : Classifier.LEFT_AFFORDANCE);
    }

    @Override
    public void onAffordanceSwipingAborted() {
    }

    @Override
    public void onStartExpandingFromPulse() {
        updateInteractionType(Classifier.PULSE_EXPAND);
    }

    @Override
    public void onExpansionFromPulseStopped() {
    }

    @Override
    public Uri reportRejectedTouch() {
        return null;
    }

    @Override
    public void onScreenOnFromTouch() {
        sessionStart();
    }

    @Override
    public boolean isReportingEnabled() {
        return false;
    }

    @Override
    public void onUnlockHintStarted() {
    }

    @Override
    public void onCameraHintStarted() {
    }

    @Override
    public void onLeftAffordanceHintStarted() {
    }

    @Override
    public void onScreenTurningOn() {
        sessionStart();
    }

    @Override
    public void onScreenOff() {
        sessionEnd();
    }


    @Override
    public void onNotificatonStopDismissing() {
    }

    @Override
    public void onNotificationDismissed() {
    }

    @Override
    public void onNotificatonStartDismissing() {
        updateInteractionType(Classifier.NOTIFICATION_DISMISS);
    }

    @Override
    public void onNotificationDoubleTap(boolean b, float v, float v1) {
    }

    @Override
    public void onBouncerShown() {
    }

    @Override
    public void onBouncerHidden() {
    }

    @Override
    public void dump(PrintWriter printWriter) {
    }

    static void logDebug(String msg) {
        logDebug(msg, null);
    }

    static void logDebug(String msg, Throwable throwable) {
        if (DEBUG) {
            Log.d(TAG, msg, throwable);
        }
    }

    static void logInfo(String msg) {
        Log.i(TAG, msg);
    }

    static void logError(String msg) {
        Log.e(TAG, msg);
    }
}
+89 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.classifier.brightline;

import static com.android.systemui.classifier.Classifier.LEFT_AFFORDANCE;
import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;

/**
 * False on swipes that are too close to 45 degrees.
 *
 * Horizontal swipes may have a different threshold than vertical.
 *
 * This falser should not run on "affordance" swipes, as they will always be close to 45.
 */
class DiagonalClassifier extends FalsingClassifier {

    private static final float HORIZONTAL_ANGLE_RANGE = (float) (5f / 360f * Math.PI * 2f);
    private static final float VERTICAL_ANGLE_RANGE = (float) (5f / 360f * Math.PI * 2f);
    private static final float DIAGONAL = (float) (Math.PI / 4); // 45 deg
    private static final float NINETY_DEG = (float) (Math.PI / 2);
    private static final float ONE_HUNDRED_EIGHTY_DEG = (float) (Math.PI);
    private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI);

    DiagonalClassifier(FalsingDataProvider dataProvider) {
        super(dataProvider);
    }

    @Override
    boolean isFalseTouch() {
        float angle = getAngle();

        if (angle == Float.MAX_VALUE) {  // Unknown angle
            return false;
        }

        if (getInteractionType() == LEFT_AFFORDANCE
                || getInteractionType() == RIGHT_AFFORDANCE) {
            return false;
        }

        float minAngle = DIAGONAL - HORIZONTAL_ANGLE_RANGE;
        float maxAngle = DIAGONAL + HORIZONTAL_ANGLE_RANGE;
        if (isVertical()) {
            minAngle = DIAGONAL - VERTICAL_ANGLE_RANGE;
            maxAngle = DIAGONAL + VERTICAL_ANGLE_RANGE;
        }

        return angleBetween(angle, minAngle, maxAngle)
                || angleBetween(angle, minAngle + NINETY_DEG, maxAngle + NINETY_DEG)
                || angleBetween(angle, minAngle - NINETY_DEG, maxAngle - NINETY_DEG)
                || angleBetween(angle, minAngle + ONE_HUNDRED_EIGHTY_DEG,
                maxAngle + ONE_HUNDRED_EIGHTY_DEG);
    }

    private boolean angleBetween(float angle, float min, float max) {
        // No need to normalize angle as it is guaranteed to be between 0 and 2*PI.
        min = normalizeAngle(min);
        max = normalizeAngle(max);

        if (min > max) {  // Can happen when angle is close to 0.
            return angle >= min || angle <= max;
        }

        return angle >= min && angle <= max;
    }

    private float normalizeAngle(float angle) {
        if (angle < 0) {
            return THREE_HUNDRED_SIXTY_DEG + (angle % THREE_HUNDRED_SIXTY_DEG);
        } else if (angle > THREE_HUNDRED_SIXTY_DEG) {
            return angle % THREE_HUNDRED_SIXTY_DEG;
        }
        return angle;
    }
}
+158 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.classifier.brightline;

import android.view.MotionEvent;
import android.view.VelocityTracker;

import java.util.List;

/**
 * Ensure that the swipe + momentum covers a minimum distance.
 */
class DistanceClassifier extends FalsingClassifier {

    private static final float HORIZONTAL_FLING_THRESHOLD_DISTANCE_IN = 1;
    private static final float VERTICAL_FLING_THRESHOLD_DISTANCE_IN = 1;
    private static final float HORIZONTAL_SWIPE_THRESHOLD_DISTANCE_IN = 3;
    private static final float VERTICAL_SWIPE_THRESHOLD_DISTANCE_IN = 3;
    private static final float VELOCITY_TO_DISTANCE = 80f;
    private static final float SCREEN_FRACTION_MIN_DISTANCE = 0.8f;

    private final float mVerticalFlingThresholdPx;
    private final float mHorizontalFlingThresholdPx;
    private final float mVerticalSwipeThresholdPx;
    private final float mHorizontalSwipeThresholdPx;

    private boolean mDistanceDirty;
    private DistanceVectors mCachedDistance;

    DistanceClassifier(FalsingDataProvider dataProvider) {
        super(dataProvider);

        mHorizontalFlingThresholdPx = Math
                .min(getWidthPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        HORIZONTAL_FLING_THRESHOLD_DISTANCE_IN * getXdpi());
        mVerticalFlingThresholdPx = Math
                .min(getHeightPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        VERTICAL_FLING_THRESHOLD_DISTANCE_IN * getYdpi());
        mHorizontalSwipeThresholdPx = Math
                .min(getWidthPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        HORIZONTAL_SWIPE_THRESHOLD_DISTANCE_IN * getXdpi());
        mVerticalSwipeThresholdPx = Math
                .min(getHeightPixels() * SCREEN_FRACTION_MIN_DISTANCE,
                        VERTICAL_SWIPE_THRESHOLD_DISTANCE_IN * getYdpi());
        mDistanceDirty = true;
    }

    private DistanceVectors getDistances() {
        if (mDistanceDirty) {
            mCachedDistance = calculateDistances();
            mDistanceDirty = false;
        }

        return mCachedDistance;
    }

    private DistanceVectors calculateDistances() {
        // This code assumes that there will be no missed DOWN or UP events.
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        List<MotionEvent> motionEvents = getRecentMotionEvents();

        if (motionEvents.size() < 3) {
            logDebug("Only " + motionEvents.size() + " motion events recorded.");
            return new DistanceVectors(0, 0, 0, 0);
        }

        for (MotionEvent motionEvent : motionEvents) {
            velocityTracker.addMovement(motionEvent);
        }
        velocityTracker.computeCurrentVelocity(1);

        float vX = velocityTracker.getXVelocity();
        float vY = velocityTracker.getYVelocity();

        velocityTracker.recycle();

        float dX = getLastMotionEvent().getX() - getFirstMotionEvent().getX();
        float dY = getLastMotionEvent().getY() - getFirstMotionEvent().getY();

        logInfo("dX: " + dX + " dY: " + dY + " xV: " + vX + " yV: " + vY);

        return new DistanceVectors(dX, dY, vX, vY);
    }

    @Override
    public void onTouchEvent(MotionEvent motionEvent) {
        mDistanceDirty = true;
    }

    @Override
    public boolean isFalseTouch() {
        return !getDistances().getPassedFlingThreshold();
    }

    boolean isLongSwipe() {
        boolean longSwipe = getDistances().getPassedDistanceThreshold();
        logDebug("Is longSwipe? " + longSwipe);
        return longSwipe;
    }

    private class DistanceVectors {
        final float mDx;
        final float mDy;
        private final float mVx;
        private final float mVy;

        DistanceVectors(float dX, float dY, float vX, float vY) {
            this.mDx = dX;
            this.mDy = dY;
            this.mVx = vX;
            this.mVy = vY;
        }

        boolean getPassedDistanceThreshold() {
            if (isHorizontal()) {
                logDebug("Horizontal swipe distance: " + Math.abs(mDx));
                logDebug("Threshold: " + mHorizontalSwipeThresholdPx);

                return Math.abs(mDx) >= mHorizontalSwipeThresholdPx;
            }

            logDebug("Vertical swipe distance: " + Math.abs(mDy));
            logDebug("Threshold: " + mVerticalSwipeThresholdPx);
            return Math.abs(mDy) >= mVerticalSwipeThresholdPx;
        }

        boolean getPassedFlingThreshold() {
            float dX = this.mDx + this.mVx * VELOCITY_TO_DISTANCE;
            float dY = this.mDy + this.mVy * VELOCITY_TO_DISTANCE;

            if (isHorizontal()) {
                logDebug("Horizontal swipe and fling distance: " + this.mDx + ", "
                        + this.mVx * VELOCITY_TO_DISTANCE);
                logDebug("Threshold: " + mHorizontalFlingThresholdPx);
                return Math.abs(dX) >= mHorizontalFlingThresholdPx;
            }

            logDebug("Vertical swipe and fling distance: " + this.mDy + ", "
                    + this.mVy * VELOCITY_TO_DISTANCE);
            logDebug("Threshold: " + mVerticalFlingThresholdPx);
            return Math.abs(dY) >= mVerticalFlingThresholdPx;
        }
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.classifier.brightline;

import android.hardware.SensorEvent;
import android.view.MotionEvent;

import com.android.systemui.classifier.Classifier;

import java.util.List;

/**
 * Base class for rules that determine False touches.
 */
abstract class FalsingClassifier {
    private final FalsingDataProvider mDataProvider;

    FalsingClassifier(FalsingDataProvider dataProvider) {
        this.mDataProvider = dataProvider;
    }

    List<MotionEvent> getRecentMotionEvents() {
        return mDataProvider.getRecentMotionEvents();
    }

    MotionEvent getFirstMotionEvent() {
        return mDataProvider.getFirstRecentMotionEvent();
    }

    MotionEvent getLastMotionEvent() {
        return mDataProvider.getLastMotionEvent();
    }

    boolean isHorizontal() {
        return mDataProvider.isHorizontal();
    }

    boolean isRight() {
        return mDataProvider.isRight();
    }

    boolean isVertical() {
        return mDataProvider.isVertical();
    }

    boolean isUp() {
        return mDataProvider.isUp();
    }

    float getAngle() {
        return mDataProvider.getAngle();
    }

    int getWidthPixels() {
        return mDataProvider.getWidthPixels();
    }

    int getHeightPixels() {
        return mDataProvider.getHeightPixels();
    }

    float getXdpi() {
        return mDataProvider.getXdpi();
    }

    float getYdpi() {
        return mDataProvider.getYdpi();
    }

    final @Classifier.InteractionType int getInteractionType() {
        return mDataProvider.getInteractionType();
    }

    final void setInteractionType(@Classifier.InteractionType int interactionType) {
        mDataProvider.setInteractionType(interactionType);
    }

    /**
     * Called whenever a MotionEvent occurs.
     *
     * Useful for classifiers that need to see every MotionEvent, but most can probably
     * use {@link #getRecentMotionEvents()} instead, which will return a list of MotionEvents.
     */
    void onTouchEvent(MotionEvent motionEvent) {};

    /**
     * Called whenever a SensorEvent occurs, specifically the ProximitySensor.
     */
    void onSensorEvent(SensorEvent sensorEvent) {};

    /**
     * The phone screen has turned on and we need to begin falsing detection.
     */
    void onSessionStarted() {};

    /**
     * The phone screen has turned off and falsing data can be discarded.
     */
    void onSessionEnded() {};

    /**
     * Returns true if the data captured so far looks like a false touch.
     */
    abstract boolean isFalseTouch();

    static void logDebug(String msg) {
        BrightLineFalsingManager.logDebug(msg);
    }

    static void logInfo(String msg) {
        BrightLineFalsingManager.logInfo(msg);
    }

    static void logError(String msg) {
        BrightLineFalsingManager.logError(msg);
    }
}
Loading