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

Commit 4cc68a30 authored by Dave Mankoff's avatar Dave Mankoff Committed by android-build-merger
Browse files

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

am: acd240fb

Change-Id: I83640ae5c6a6681eb33c87a70a629573f189d024
parents f351094e acd240fb
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