diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java index d58b95c6220f4..d1494dfb107e9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java @@ -23,10 +23,16 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; +import com.android.systemui.Gefingerpoken; + +import java.util.ArrayList; +import java.util.List; + /** * A Base class for all Keyguard password/pattern/pin related inputs. */ public abstract class KeyguardInputView extends LinearLayout { + private final List mMotionEventListener = new ArrayList<>(); public KeyguardInputView(Context context) { super(context); @@ -56,4 +62,25 @@ public abstract class KeyguardInputView extends LinearLayout { boolean startDisappearAnimation(Runnable finishRunnable) { return false; } + + void addMotionEventListener(Gefingerpoken listener) { + mMotionEventListener.add(listener); + } + + void removeMotionEventListener(Gefingerpoken listener) { + mMotionEventListener.remove(listener); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return mMotionEventListener.stream().anyMatch(listener -> listener.onTouchEvent(event)) + || super.onTouchEvent(event); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return mMotionEventListener.stream().anyMatch( + listener -> listener.onInterceptTouchEvent(event)) + || super.onInterceptTouchEvent(event); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 6aa5e0df3653b..1c691e7098a11 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -26,6 +26,7 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -155,6 +156,7 @@ public abstract class KeyguardInputViewController private final Resources mResources; private LiftToActivateListener mLiftToActivateListener; private TelephonyManager mTelephonyManager; + private final FalsingCollector mFalsingCollector; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -163,7 +165,7 @@ public abstract class KeyguardInputViewController KeyguardMessageAreaController.Factory messageAreaControllerFactory, InputMethodManager inputMethodManager, @Main DelayableExecutor mainExecutor, @Main Resources resources, LiftToActivateListener liftToActivateListener, - TelephonyManager telephonyManager) { + TelephonyManager telephonyManager, FalsingCollector falsingCollector) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -173,6 +175,7 @@ public abstract class KeyguardInputViewController mResources = resources; mLiftToActivateListener = liftToActivateListener; mTelephonyManager = telephonyManager; + mFalsingCollector = falsingCollector; } /** Create a new {@link KeyguardInputViewController}. */ @@ -191,17 +194,17 @@ public abstract class KeyguardInputViewController return new KeyguardPinViewController((KeyguardPINView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, - mLiftToActivateListener); + mLiftToActivateListener, mFalsingCollector); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, - mLiftToActivateListener, mTelephonyManager); + mLiftToActivateListener, mTelephonyManager, mFalsingCollector); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, - mLiftToActivateListener, mTelephonyManager); + mLiftToActivateListener, mTelephonyManager, mFalsingCollector); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 4d0ebfffbe04f..f2479488db0f8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -25,12 +25,15 @@ import android.view.View.OnTouchListener; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.systemui.Gefingerpoken; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; public abstract class KeyguardPinBasedInputViewController extends KeyguardAbsKeyInputViewController { private final LiftToActivateListener mLiftToActivateListener; + private final FalsingCollector mFalsingCollector; protected PasswordTextView mPasswordEntry; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { @@ -40,13 +43,26 @@ public abstract class KeyguardPinBasedInputViewController { + private final OnTouchListener mActionButtonTouchListener = (v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mView.doHapticKeyClick(); } return false; }; + private final Gefingerpoken mGlobalTouchListener = new Gefingerpoken() { + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mFalsingCollector.avoidGesture(); + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return false; + } + }; + protected KeyguardPinBasedInputViewController(T view, KeyguardUpdateMonitor keyguardUpdateMonitor, SecurityMode securityMode, @@ -54,10 +70,12 @@ public abstract class KeyguardPinBasedInputViewController { // check for time-based lockouts if (mPasswordEntry.isEnabled()) { @@ -87,7 +107,7 @@ public abstract class KeyguardPinBasedInputViewController { @@ -32,10 +33,11 @@ public class KeyguardPinViewController SecurityMode securityMode, LockPatternUtils lockPatternUtils, KeyguardSecurityCallback keyguardSecurityCallback, KeyguardMessageAreaController.Factory messageAreaControllerFactory, - LatencyTracker latencyTracker, - LiftToActivateListener liftToActivateListener) { + LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, + FalsingCollector falsingCollector) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, - messageAreaControllerFactory, latencyTracker, liftToActivateListener); + messageAreaControllerFactory, latencyTracker, liftToActivateListener, + falsingCollector); mKeyguardUpdateMonitor = keyguardUpdateMonitor; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 5b4a7ff3e16ed..b218141346074 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -38,6 +38,7 @@ import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; public class KeyguardSimPinViewController extends KeyguardPinBasedInputViewController { @@ -76,11 +77,11 @@ public class KeyguardSimPinViewController SecurityMode securityMode, LockPatternUtils lockPatternUtils, KeyguardSecurityCallback keyguardSecurityCallback, KeyguardMessageAreaController.Factory messageAreaControllerFactory, - LatencyTracker latencyTracker, - LiftToActivateListener liftToActivateListener, - TelephonyManager telephonyManager) { + LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, + TelephonyManager telephonyManager, FalsingCollector falsingCollector) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, - messageAreaControllerFactory, latencyTracker, liftToActivateListener); + messageAreaControllerFactory, latencyTracker, liftToActivateListener, + falsingCollector); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index eafb33f8195d4..890a17c1cf921 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -39,6 +39,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.classifier.FalsingCollector; public class KeyguardSimPukViewController extends KeyguardPinBasedInputViewController { @@ -83,11 +84,11 @@ public class KeyguardSimPukViewController SecurityMode securityMode, LockPatternUtils lockPatternUtils, KeyguardSecurityCallback keyguardSecurityCallback, KeyguardMessageAreaController.Factory messageAreaControllerFactory, - LatencyTracker latencyTracker, - LiftToActivateListener liftToActivateListener, - TelephonyManager telephonyManager) { + LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, + TelephonyManager telephonyManager, FalsingCollector falsingCollector) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, - messageAreaControllerFactory, latencyTracker, liftToActivateListener); + messageAreaControllerFactory, latencyTracker, liftToActivateListener, + falsingCollector); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index e78057f5bd71a..6572ca95b5c31 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -18,6 +18,7 @@ package com.android.systemui.classifier; import static com.android.systemui.classifier.FalsingManagerProxy.FALSING_SUCCESS; import static com.android.systemui.classifier.FalsingModule.BRIGHT_LINE_GESTURE_CLASSIFERS; +import static com.android.systemui.classifier.FalsingModule.DOUBLE_TAP_TIMEOUT_MS; import android.net.Uri; import android.os.Build; @@ -28,23 +29,26 @@ import androidx.annotation.NonNull; import com.android.internal.logging.MetricsLogger; import com.android.systemui.classifier.FalsingDataProvider.SessionListener; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.TestHarness; import com.android.systemui.dock.DockManager; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.sensors.ThresholdSensor; -import com.android.systemui.util.time.SystemClock; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Queue; import java.util.Set; import java.util.StringJoiner; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Named; @@ -65,7 +69,8 @@ public class BrightLineFalsingManager implements FalsingManager { private final SingleTapClassifier mSingleTapClassifier; private final DoubleTapClassifier mDoubleTapClassifier; private final HistoryTracker mHistoryTracker; - private final SystemClock mSystemClock; + private final DelayableExecutor mDelayableExecutor; + private final long mDoubleTapTimeMs; private final boolean mTestHarness; private final MetricsLogger mMetricsLogger; private int mIsFalseTouchCalls; @@ -90,23 +95,44 @@ public class BrightLineFalsingManager implements FalsingManager { private final FalsingDataProvider.GestureCompleteListener mGestureCompleteListener = new FalsingDataProvider.GestureCompleteListener() { - @Override - public void onGestureComplete() { - mHistoryTracker.addResults( - mClassifiers.stream().map(FalsingClassifier::classifyGesture) - .collect(Collectors.toCollection(ArrayList::new)), - mSystemClock.uptimeMillis()); + @Override + public void onGestureComplete(long completionTimeMs) { + if (mPriorResults != null) { + // Single taps that may become double taps don't get added right away. + if (mClassifyAsSingleTap) { + Collection singleTapResults = mPriorResults; + mSingleTapHistoryCanceller = mDelayableExecutor.executeDelayed( + () -> { + mSingleTapHistoryCanceller = null; + mHistoryTracker.addResults(singleTapResults, completionTimeMs); + }, + mDoubleTapTimeMs); + mClassifyAsSingleTap = false; // Don't treat things as single taps by default. + } else { + mHistoryTracker.addResults(mPriorResults, completionTimeMs); + } + mPriorResults = null; + } else { + // Gestures that were not classified get treated as a false. + mHistoryTracker.addResults( + Collections.singleton( + FalsingClassifier.Result.falsed(.8, "unclassified")), + completionTimeMs); + } } }; - private boolean mPreviousResult = false; + private Collection mPriorResults; + private boolean mClassifyAsSingleTap; + private Runnable mSingleTapHistoryCanceller; @Inject public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, DockManager dockManager, MetricsLogger metricsLogger, @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set classifiers, SingleTapClassifier singleTapClassifier, DoubleTapClassifier doubleTapClassifier, - HistoryTracker historyTracker, SystemClock systemClock, + HistoryTracker historyTracker, @Main DelayableExecutor delayableExecutor, + @Named(DOUBLE_TAP_TIMEOUT_MS) long doubleTapTimeMs, @TestHarness boolean testHarness) { mDataProvider = falsingDataProvider; mDockManager = dockManager; @@ -115,7 +141,8 @@ public class BrightLineFalsingManager implements FalsingManager { mSingleTapClassifier = singleTapClassifier; mDoubleTapClassifier = doubleTapClassifier; mHistoryTracker = historyTracker; - mSystemClock = systemClock; + mDelayableExecutor = delayableExecutor; + mDoubleTapTimeMs = doubleTapTimeMs; mTestHarness = testHarness; mDataProvider.addSessionListener(mSessionListener); @@ -129,38 +156,51 @@ public class BrightLineFalsingManager implements FalsingManager { @Override public boolean isFalseTouch(@Classifier.InteractionType int interactionType) { + boolean result; + + mClassifyAsSingleTap = false; mDataProvider.setInteractionType(interactionType); - if (!mDataProvider.isDirty()) { - return mPreviousResult; + + if (!mTestHarness && !mDataProvider.isJustUnlockedWithFace() && !mDockManager.isDocked()) { + Stream results = + mClassifiers.stream().map(falsingClassifier -> { + FalsingClassifier.Result classifierResult = + falsingClassifier.classifyGesture( + mHistoryTracker.falsePenalty(), + mHistoryTracker.falseConfidence()); + if (classifierResult.isFalse()) { + logInfo(String.format( + (Locale) null, + "{classifier=%s, interactionType=%d}", + falsingClassifier.getClass().getName(), + mDataProvider.getInteractionType())); + String reason = classifierResult.getReason(); + if (reason != null) { + logInfo(reason); + } + } else { + logDebug(falsingClassifier.getClass().getName() + ": false"); + } + return classifierResult; + }); + mPriorResults = new ArrayList<>(); + final boolean[] localResult = {false}; + results.forEach(classifierResult -> { + localResult[0] |= classifierResult.isFalse(); + mPriorResults.add(classifierResult); + }); + result = localResult[0]; + } else { + result = false; + mPriorResults = Collections.singleton(FalsingClassifier.Result.passed(1)); } - mPreviousResult = !mTestHarness - && !mDataProvider.isJustUnlockedWithFace() && !mDockManager.isDocked() - && mClassifiers.stream().anyMatch(falsingClassifier -> { - FalsingClassifier.Result result = falsingClassifier.classifyGesture( - mHistoryTracker.falsePenalty(), mHistoryTracker.falseConfidence()); - if (result.isFalse()) { - logInfo(String.format( - (Locale) null, - "{classifier=%s, interactionType=%d}", - falsingClassifier.getClass().getName(), - mDataProvider.getInteractionType())); - String reason = result.getReason(); - if (reason != null) { - logInfo(reason); - } - } else { - logDebug(falsingClassifier.getClass().getName() + ": false"); - } - return result.isFalse(); - }); - - logDebug("Is false touch? " + mPreviousResult); + logDebug("Is false touch? " + result); if (Build.IS_ENG || Build.IS_USERDEBUG) { // Copy motion events, as the passed in list gets emptied out elsewhere in the code. RECENT_SWIPES.add(new DebugSwipeRecord( - mPreviousResult, + result, mDataProvider.getInteractionType(), mDataProvider.getRecentMotionEvents().stream().map( motionEvent -> new XYDt( @@ -173,13 +213,16 @@ public class BrightLineFalsingManager implements FalsingManager { } } - return mPreviousResult; + return result; } @Override public boolean isFalseTap(boolean robustCheck) { + mClassifyAsSingleTap = true; + FalsingClassifier.Result singleTapResult = mSingleTapClassifier.isTap(mDataProvider.getRecentMotionEvents()); + mPriorResults = Collections.singleton(singleTapResult); if (singleTapResult.isFalse()) { logInfo(String.format( (Locale) null, "{classifier=%s}", mSingleTapClassifier.getClass().getName())); @@ -192,7 +235,12 @@ public class BrightLineFalsingManager implements FalsingManager { // TODO(b/172655679): More heuristics to come. For now, allow touches through if face-authed if (robustCheck) { - return !mDataProvider.isJustUnlockedWithFace(); + boolean result = !mDataProvider.isJustUnlockedWithFace(); + mPriorResults = Collections.singleton( + result ? FalsingClassifier.Result.falsed(0.1, "no face detected") + : FalsingClassifier.Result.passed(1)); + + return result; } return false; @@ -200,7 +248,9 @@ public class BrightLineFalsingManager implements FalsingManager { @Override public boolean isFalseDoubleTap() { + mClassifyAsSingleTap = false; FalsingClassifier.Result result = mDoubleTapClassifier.classifyGesture(); + mPriorResults = Collections.singleton(result); if (result.isFalse()) { logInfo(String.format( (Locale) null, "{classifier=%s}", mDoubleTapClassifier.getClass().getName())); @@ -208,6 +258,12 @@ public class BrightLineFalsingManager implements FalsingManager { if (reason != null) { logInfo(reason); } + } else { + // A valid double tap prevents an invalid single tap from going into history. + if (mSingleTapHistoryCanceller != null) { + mSingleTapHistoryCanceller.run(); + mSingleTapHistoryCanceller = null; + } } return result.isFalse(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index fe47162a1d4ec..b0bbab366e93a 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -115,6 +115,9 @@ public interface FalsingCollector { /** */ void onTouchEvent(MotionEvent ev); + /** */ + void avoidGesture(); + /** */ void cleanup(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index fd059898f7a6b..12a0604391061 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -146,6 +146,10 @@ public class FalsingCollectorFake implements FalsingCollector { public void onTouchEvent(MotionEvent ev) { } + @Override + public void avoidGesture() { + } + @Override public void cleanup() { } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index 4c11ecf669565..e08b43b3521f5 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -49,6 +49,8 @@ class FalsingCollectorImpl implements FalsingCollector { private boolean mShowingAod; private boolean mScreenOn; private boolean mSessionStarted; + private MotionEvent mPendingDownEvent; + private boolean mAvoidGesture; private final ThresholdSensor.Listener mSensorEventListener = this::onProximityEvent; @@ -245,7 +247,32 @@ class FalsingCollectorImpl implements FalsingCollector { @Override public void onTouchEvent(MotionEvent ev) { - mFalsingDataProvider.onMotionEvent(ev); + // We delay processing down events to see if another component wants to process them. + // If #avoidGesture is called after a MotionEvent.ACTION_DOWN, all following motion events + // will be ignored by the collector until another MotionEvent.ACTION_DOWN is passed in. + // avoidGesture must be called immediately following the MotionEvent.ACTION_DOWN, before + // any other events are processed, otherwise the whole gesture will be recorded. + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + // Make a copy of ev, since it will be recycled after we exit this method. + mPendingDownEvent = MotionEvent.obtain(ev); + mAvoidGesture = false; + } else if (!mAvoidGesture) { + if (mPendingDownEvent != null) { + mFalsingDataProvider.onMotionEvent(mPendingDownEvent); + mPendingDownEvent.recycle(); + mPendingDownEvent = null; + } + mFalsingDataProvider.onMotionEvent(ev); + } + } + + @Override + public void avoidGesture() { + if (mPendingDownEvent != null) { + mAvoidGesture = true; + mPendingDownEvent.recycle(); + mPendingDownEvent = null; + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index deb9e6dc442ca..4bacc15984905 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -90,10 +90,8 @@ public class FalsingDataProvider { } if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - if (!mRecentMotionEvents.isEmpty()) { - mExtendedMotionEvents.addFirst(mRecentMotionEvents); - mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); - } + completePriorGesture(); + mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); } mRecentMotionEvents.addAll(motionEvents); @@ -101,9 +99,25 @@ public class FalsingDataProvider { mMotionEventListeners.forEach(listener -> listener.onMotionEvent(motionEvent)); + // We explicitly do not complete a gesture on UP or CANCEL events. + // We wait for the next gesture to start before marking the prior gesture as complete. This + // has multiple benefits. First, it makes it trivial to track the "current" or "recent" + // gesture, as it will always be found in mRecentMotionEvents. Second, and most importantly, + // it ensures that the current gesture doesn't get added to this HistoryTracker before it + // is analyzed. + mDirty = true; } + private void completePriorGesture() { + if (!mRecentMotionEvents.isEmpty()) { + mGestuerCompleteListeners.forEach(listener -> listener.onGestureComplete( + mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getEventTime())); + + mExtendedMotionEvents.addFirst(mRecentMotionEvents); + } + } + /** Returns screen width in pixels. */ public int getWidthPixels() { return mWidthPixels; @@ -146,13 +160,6 @@ public class FalsingDataProvider { } } - /** - * Returns true if new data has been supplied since the last time this class has been accessed. - */ - public boolean isDirty() { - return mDirty; - } - /** Return the interaction type that is being compared against for falsing. */ public final int getInteractionType() { return mInteractionType; @@ -387,6 +394,6 @@ public class FalsingDataProvider { /** Callback to be alerted when the current gesture ends. */ public interface GestureCompleteListener { /** */ - void onGestureComplete(); + void onGestureComplete(long completionTimeMs); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 4944284698a05..31cc7bb7c958e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -31,6 +31,8 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.classifier.FalsingCollectorFake; import org.junit.Before; import org.junit.Test; @@ -65,6 +67,7 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { private LatencyTracker mLatencyTracker; @Mock private LiftToActivateListener mLiftToactivateListener; + private FalsingCollector mFalsingCollector = new FalsingCollectorFake(); @Mock private View mDeleteButton; @Mock @@ -88,7 +91,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { .thenReturn(mOkButton); mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, - mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener) { + mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, + mFalsingCollector) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index 8c547b127da20..5709ce3035a2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -16,12 +16,15 @@ package com.android.systemui.classifier; -import static com.android.systemui.util.mockito.KotlinMockitoHelpersKt.any; - import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,6 +38,7 @@ import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingDataProvider.GestureCompleteListener; import com.android.systemui.dock.DockManagerFake; +import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; @@ -45,7 +49,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,6 +56,8 @@ import java.util.Set; @SmallTest @RunWith(AndroidTestingRunner.class) public class BrightLineClassifierTest extends SysuiTestCase { + private static final long DOUBLE_TAP_TIMEOUT_MS = 1000; + private BrightLineFalsingManager mBrightLineFalsingManager; @Mock private FalsingDataProvider mFalsingDataProvider; @@ -69,24 +74,35 @@ public class BrightLineClassifierTest extends SysuiTestCase { private FalsingClassifier mClassifierB; private final List mMotionEventList = new ArrayList<>(); @Mock - private HistoryTracker mHistoryTracker; - private FakeSystemClock mSystemClock = new FakeSystemClock(); + private HistoryTracker mHistoryTracker;; + private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private final FalsingClassifier.Result mFalsedResult = FalsingClassifier.Result.falsed(1, ""); private final FalsingClassifier.Result mPassedResult = FalsingClassifier.Result.passed(1); + private GestureCompleteListener mGestureCompleteListener; @Before public void setup() { MockitoAnnotations.initMocks(this); when(mClassifierA.classifyGesture(anyDouble(), anyDouble())).thenReturn(mPassedResult); when(mClassifierB.classifyGesture(anyDouble(), anyDouble())).thenReturn(mPassedResult); + when(mSingleTapClassfier.isTap(any(List.class))).thenReturn(mPassedResult); + when(mDoubleTapClassifier.classifyGesture()).thenReturn(mPassedResult); mClassifiers.add(mClassifierA); mClassifiers.add(mClassifierB); - when(mFalsingDataProvider.isDirty()).thenReturn(true); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mDockManager, mMetricsLogger, mClassifiers, mSingleTapClassfier, mDoubleTapClassifier, - mHistoryTracker, mSystemClock, false); + mHistoryTracker, mFakeExecutor, DOUBLE_TAP_TIMEOUT_MS, false); + + + ArgumentCaptor gestureCompleteListenerCaptor = + ArgumentCaptor.forClass(GestureCompleteListener.class); + + verify(mFalsingDataProvider).addGestureCompleteListener( + gestureCompleteListenerCaptor.capture()); + + mGestureCompleteListener = gestureCompleteListenerCaptor.getValue(); } @Test @@ -179,15 +195,57 @@ public class BrightLineClassifierTest extends SysuiTestCase { @Test public void testHistory() { - ArgumentCaptor gestureCompleteListenerCaptor = - ArgumentCaptor.forClass(GestureCompleteListener.class); + mGestureCompleteListener.onGestureComplete(1000); - verify(mFalsingDataProvider).addGestureCompleteListener( - gestureCompleteListenerCaptor.capture()); + verify(mHistoryTracker).addResults(anyCollection(), eq(1000L)); + } - GestureCompleteListener gestureCompleteListener = gestureCompleteListenerCaptor.getValue(); - gestureCompleteListener.onGestureComplete(); + @Test + public void testHistory_singleTap() { + // When trying to classify single taps, we don't immediately add results to history. + mBrightLineFalsingManager.isFalseTap(false); + mGestureCompleteListener.onGestureComplete(1000); - verify(mHistoryTracker).addResults(any(Collection.class), eq(mSystemClock.uptimeMillis())); + verify(mHistoryTracker, never()).addResults(any(), anyLong()); + + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + + verify(mHistoryTracker).addResults(anyCollection(), eq(1000L)); + } + + @Test + public void testHistory_multipleSingleTaps() { + // When trying to classify single taps, we don't immediately add results to history. + mBrightLineFalsingManager.isFalseTap(false); + mGestureCompleteListener.onGestureComplete(1000); + mBrightLineFalsingManager.isFalseTap(false); + mGestureCompleteListener.onGestureComplete(2000); + + verify(mHistoryTracker, never()).addResults(any(), anyLong()); + + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runNextReady(); + verify(mHistoryTracker).addResults(anyCollection(), eq(1000L)); + reset(mHistoryTracker); + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runNextReady(); + verify(mHistoryTracker).addResults(anyCollection(), eq(2000L)); + } + + @Test + public void testHistory_doubleTap() { + // When trying to classify single taps, we don't immediately add results to history. + mBrightLineFalsingManager.isFalseTap(false); + mGestureCompleteListener.onGestureComplete(1000); + // Before checking for double tap, we may check for single-tap on the second gesture. + mBrightLineFalsingManager.isFalseTap(false); + mBrightLineFalsingManager.isFalseDoubleTap(); + mGestureCompleteListener.onGestureComplete(2000); + + // Double tap is immediately added to history. Single tap is never added. + verify(mHistoryTracker).addResults(anyCollection(), eq(2000L)); + + assertThat(mFakeExecutor.numPending()).isEqualTo(0); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index af5e789a0c41e..23ef865750b2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -17,12 +17,15 @@ package com.android.systemui.classifier; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -38,6 +41,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -66,7 +70,6 @@ public class FalsingCollectorImplTest extends SysuiTestCase { mKeyguardUpdateMonitor, mProximitySensor, mStatusBarStateController); } - @Test public void testRegisterSensor() { mFalsingCollector.onScreenTurningOn(); @@ -110,7 +113,6 @@ public class FalsingCollectorImplTest extends SysuiTestCase { @Test public void testUnregisterSensor_StateTransition() { - ArgumentCaptor stateListenerArgumentCaptor = ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); verify(mStatusBarStateController).addCallback(stateListenerArgumentCaptor.capture()); @@ -120,4 +122,37 @@ public class FalsingCollectorImplTest extends SysuiTestCase { stateListenerArgumentCaptor.getValue().onStateChanged(StatusBarState.SHADE); verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class)); } + + @Test + public void testPassThroughGesture() { + MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); + + // Nothing passed initially + mFalsingCollector.onTouchEvent(down); + verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class)); + + // Up event flushes the down event. + mFalsingCollector.onTouchEvent(up); + InOrder orderedCalls = inOrder(mFalsingDataProvider); + // We can't simply use "eq" or similar because the collector makes a copy of "down". + orderedCalls.verify(mFalsingDataProvider).onMotionEvent( + argThat(argument -> argument.getActionMasked() == MotionEvent.ACTION_DOWN)); + orderedCalls.verify(mFalsingDataProvider).onMotionEvent(up); + } + + @Test + public void testAvoidGesture() { + MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + + // Nothing passed initially + mFalsingCollector.onTouchEvent(down); + verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class)); + + mFalsingCollector.avoidGesture(); + // Up event would flush, but we were told to avoid. + mFalsingCollector.onTouchEvent(up); + verify(mFalsingDataProvider, never()).onMotionEvent(any(MotionEvent.class)); + } }