From 8848abd1b5690ab922254ce5401bb6691f771548 Mon Sep 17 00:00:00 2001 From: Dave Mankoff Date: Wed, 9 Dec 2020 11:44:47 -0500 Subject: [PATCH 1/2] Add ability to ignore gestures in FalsingManager This gives other parts of the system the ability to temporarily override the falsing manager, telling it to ignore a given gesture. The intent is to give other parts of the system a chance to include their own falsing information (such as whether a button was tapped or not), giving the FalsingManager more context about what is going on. As of right now, their is no way to feedback this context-aware falsing information back to the FalsingManager. An API for this is still being explored. Bug: 172655679 Test: atest SystemUITests && manual Change-Id: I87ee94a2386622ebff7580346325ae07e3bb5f5d --- .../android/keyguard/KeyguardInputView.java | 27 +++++++++++++ .../keyguard/KeyguardInputViewController.java | 11 ++++-- .../KeyguardPinBasedInputViewController.java | 28 +++++++++++-- .../keyguard/KeyguardPinViewController.java | 8 ++-- .../KeyguardSimPinViewController.java | 9 +++-- .../KeyguardSimPukViewController.java | 9 +++-- .../systemui/classifier/FalsingCollector.java | 3 ++ .../classifier/FalsingCollectorFake.java | 4 ++ .../classifier/FalsingCollectorImpl.java | 29 +++++++++++++- ...yguardPinBasedInputViewControllerTest.java | 6 ++- .../classifier/FalsingCollectorImplTest.java | 39 ++++++++++++++++++- 11 files changed, 150 insertions(+), 23 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputView.java index d42a53cc875e0..e7114a962cfbc 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); @@ -52,4 +58,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..df8709c12442e 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/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..75fa6f19bce05 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/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/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)); + } } From 7860852d3b2c1a52cbfb31403951a316fceb3d2d Mon Sep 17 00:00:00 2001 From: Dave Mankoff Date: Tue, 15 Dec 2020 12:23:37 -0500 Subject: [PATCH 2/2] Connect FalsingManager to HistoryTracker. With this change, the analysis of gestures actually gets added to our HistoryTracker. Prior to this, HistoryTracker was only ever being exercised in tests. The one trick that this addresses is that invalid single-taps can't immediately be added to the HistoryTracker, as they may become _valid_ double taps. We don't want double taps to be penalized. Bug: 172655679 Test: atest SystemUITests Change-Id: I2e5ece6af82eb20f053b6b17298dcd9002236e39 --- .../KeyguardPinBasedInputViewController.java | 6 + .../classifier/BrightLineFalsingManager.java | 132 +++++++++++++----- .../classifier/FalsingCollectorImpl.java | 2 +- .../classifier/FalsingDataProvider.java | 31 ++-- .../classifier/BrightLineClassifierTest.java | 86 ++++++++++-- 5 files changed, 192 insertions(+), 65 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index df8709c12442e..f2479488db0f8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -120,6 +120,12 @@ public abstract class KeyguardPinBasedInputViewController 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/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index 75fa6f19bce05..e08b43b3521f5 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -249,7 +249,7 @@ class FalsingCollectorImpl implements FalsingCollector { public void onTouchEvent(MotionEvent 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. + // 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) { 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/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); } }