diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java index 63f8b1f5dbb8e..16184a807bc7f 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java @@ -30,7 +30,7 @@ import java.io.PrintWriter; */ @ProvidesInterface(version = FalsingManager.VERSION) public interface FalsingManager { - int VERSION = 5; + int VERSION = 6; void onSuccessfulUnlock(); @@ -45,6 +45,42 @@ public interface FalsingManager { /** Returns true if the gesture should be rejected. */ boolean isFalseTouch(int interactionType); + /** + * Returns true if the FalsingManager thinks the last gesure was not a valid tap. + * + * Accepts one parameter, robustCheck, that distinctly changes behavior. When set to false, + * this method simply looks at the last gesture and returns whether it is a tap or not, (as + * opposed to a swipe or other non-tap gesture). When set to true, a more thorough analysis + * is performed that can include historical interactions and other contextual cues to see + * if the tap looks accidental. + * + * Set robustCheck to true if you want to validate a tap for launching an action, like opening + * a notification. Set to false if you simply want to know if the last gesture looked like a + * tap. + */ + boolean isFalseTap(boolean robustCheck); + + /** + * Returns true if the last two gestures do not look like a double tap. + * + * Only works on data that has already been reported to the FalsingManager. Be sure that + * {@link #onTouchEvent(MotionEvent, int, int)} has already been called for all of the + * taps you want considered. + * + * This looks at the last two gestures on the screen, ensuring that they meet the following + * criteria: + * + * a) There are at least two gestures. + * b) The last two gestures look like taps. + * c) The last two gestures look like a double tap taken together. + * + * This method is _not_ context aware. That is to say, if two taps occur on two neighboring + * views, but are otherwise close to one another, this will report a successful double tap. + * It is up to the caller to decide + * @return + */ + boolean isFalseDoubleTap(); + void onNotificatonStopDraggingDown(); void setNotificationExpanded(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java index 6961b45c3c375..87ee4d96c0ce5 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java @@ -29,10 +29,13 @@ import java.io.PrintWriter; */ public class FalsingManagerFake implements FalsingManager { private boolean mIsFalseTouch; + private boolean mIsFalseTap; + private boolean mIsFalseDoubleTap; private boolean mIsUnlockingDisabled; private boolean mIsClassiferEnabled; private boolean mShouldEnforceBouncer; private boolean mIsReportingEnabled; + private boolean mIsFalseRobustTap; @Override public void onSuccessfulUnlock() { @@ -74,6 +77,28 @@ public class FalsingManagerFake implements FalsingManager { return mIsFalseTouch; } + public void setFalseRobustTap(boolean falseRobustTap) { + mIsFalseRobustTap = falseRobustTap; + } + + public void setFalseTap(boolean falseTap) { + mIsFalseTap = falseTap; + } + + public void setFalseDoubleTap(boolean falseDoubleTap) { + mIsFalseDoubleTap = falseDoubleTap; + } + + @Override + public boolean isFalseTap(boolean robustCheck) { + return robustCheck ? mIsFalseRobustTap : mIsFalseTap; + } + + @Override + public boolean isFalseDoubleTap() { + return mIsFalseDoubleTap; + } + @Override public void onNotificatonStopDraggingDown() { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java index decaec10e572f..db58a45bf54e6 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java @@ -328,6 +328,16 @@ public class FalsingManagerImpl implements FalsingManager { return isFalse; } + @Override + public boolean isFalseTap(boolean robustCheck) { + return true; + } + + @Override + public boolean isFalseDoubleTap() { + return false; + } + private void clearPendingWtf() { if (mPendingWtf != null) { mHandler.removeCallbacks(mPendingWtf); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java index 2c31862e9b79e..f8741bf7b553e 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java @@ -19,10 +19,12 @@ package com.android.systemui.classifier; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_MANAGER_ENABLED; import android.content.Context; +import android.content.res.Resources; import android.hardware.SensorManager; import android.net.Uri; import android.provider.DeviceConfig; import android.view.MotionEvent; +import android.view.ViewConfiguration; import androidx.annotation.NonNull; @@ -61,6 +63,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { private static final String PROXIMITY_SENSOR_TAG = "FalsingManager"; private final ProximitySensor mProximitySensor; + private final Resources mResources; + private final ViewConfiguration mViewConfiguration; private final FalsingDataProvider mFalsingDataProvider; private FalsingManager mInternalFalsingManager; private DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener; @@ -79,12 +83,16 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { DumpManager dumpManager, @UiBackground Executor uiBgExecutor, StatusBarStateController statusBarStateController, + @Main Resources resources, + ViewConfiguration viewConfiguration, FalsingDataProvider falsingDataProvider) { mProximitySensor = proximitySensor; mDockManager = dockManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mUiBgExecutor = uiBgExecutor; mStatusBarStateController = statusBarStateController; + mResources = resources; + mViewConfiguration = viewConfiguration; mFalsingDataProvider = falsingDataProvider; mProximitySensor.setTag(PROXIMITY_SENSOR_TAG); mProximitySensor.setDelay(SensorManager.SENSOR_DELAY_GAME); @@ -147,6 +155,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { mKeyguardUpdateMonitor, mProximitySensor, mDeviceConfig, + mResources, + mViewConfiguration, mDockManager, mStatusBarStateController ); @@ -191,6 +201,17 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { return mInternalFalsingManager.isFalseTouch(interactionType); } + + @Override + public boolean isFalseTap(boolean robustCheck) { + return mInternalFalsingManager.isFalseTap(robustCheck); + } + + @Override + public boolean isFalseDoubleTap() { + return mInternalFalsingManager.isFalseDoubleTap(); + } + @Override public void onNotificatonStopDraggingDown() { mInternalFalsingManager.onNotificatonStartDraggingDown(); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java index 9d847ca62465d..fe295fbdb7107 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java @@ -20,21 +20,26 @@ import static com.android.systemui.classifier.FalsingManagerImpl.FALSING_REMAIN_ import static com.android.systemui.classifier.FalsingManagerImpl.FALSING_SUCCESS; import android.app.ActivityManager; +import android.content.res.Resources; import android.hardware.biometrics.BiometricSourceType; import android.net.Uri; import android.os.Build; import android.util.Log; import android.view.MotionEvent; +import android.view.ViewConfiguration; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.IndentingPrintWriter; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.R; import com.android.systemui.classifier.Classifier; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.phone.NotificationTapHelper; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.sensors.ProximitySensor; import com.android.systemui.util.sensors.ThresholdSensor; @@ -64,6 +69,8 @@ public class BrightLineFalsingManager implements FalsingManager { private final ProximitySensor mProximitySensor; private final DockManager mDockManager; private final StatusBarStateController mStatusBarStateController; + private final SingleTapClassifier mSingleTapClassifier; + private final DoubleTapClassifier mDoubleTapClassifier; private boolean mSessionStarted; private MetricsLogger mMetricsLogger; private int mIsFalseTouchCalls; @@ -106,8 +113,9 @@ public class BrightLineFalsingManager implements FalsingManager { public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, KeyguardUpdateMonitor keyguardUpdateMonitor, ProximitySensor proximitySensor, - DeviceConfigProxy deviceConfigProxy, - DockManager dockManager, StatusBarStateController statusBarStateController) { + DeviceConfigProxy deviceConfigProxy, @Main Resources resources, + ViewConfiguration viewConfiguration, DockManager dockManager, + StatusBarStateController statusBarStateController) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mDataProvider = falsingDataProvider; mProximitySensor = proximitySensor; @@ -129,6 +137,12 @@ public class BrightLineFalsingManager implements FalsingManager { mClassifiers.add(distanceClassifier); mClassifiers.add(proximityClassifier); mClassifiers.add(new ZigZagClassifier(mDataProvider, deviceConfigProxy)); + + mSingleTapClassifier = new SingleTapClassifier( + mDataProvider, viewConfiguration.getScaledTouchSlop()); + mDoubleTapClassifier = new DoubleTapClassifier(mDataProvider, mSingleTapClassifier, + resources.getDimension(R.dimen.double_tap_slop), + NotificationTapHelper.DOUBLE_TAP_TIMEOUT_MS); } private void registerSensors() { @@ -236,6 +250,36 @@ public class BrightLineFalsingManager implements FalsingManager { return mPreviousResult; } + @Override + public boolean isFalseTap(boolean robustCheck) { + if (!mSingleTapClassifier.isTap(mDataProvider.getRecentMotionEvents())) { + logInfo(String.format( + (Locale) null, "{classifier=%s}", mSingleTapClassifier.getClass().getName())); + String reason = mSingleTapClassifier.getReason(); + if (reason != null) { + logInfo(reason); + } + return true; + } + + // TODO(b/172655679): we always reject single-taps when doing a robust check for now. + return robustCheck; + } + + @Override + public boolean isFalseDoubleTap() { + boolean result = mDoubleTapClassifier.isFalseTouch(); + if (result) { + logInfo(String.format( + (Locale) null, "{classifier=%s}", mDoubleTapClassifier.getClass().getName())); + String reason = mDoubleTapClassifier.getReason(); + if (reason != null) { + logInfo(reason); + } + } + return result; + } + @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 diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/DoubleTapClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DoubleTapClassifier.java new file mode 100644 index 0000000000000..d3af1c347048c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/DoubleTapClassifier.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 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 java.util.List; +import java.util.Queue; + +/** + * Returns a false touch if the most two recent gestures are not taps or are too far apart. + */ +public class DoubleTapClassifier extends FalsingClassifier { + + private final SingleTapClassifier mSingleTapClassifier; + private final float mDoubleTapSlop; + private final long mDoubleTapTimeMs; + + private StringBuilder mReason = new StringBuilder(); + + DoubleTapClassifier(FalsingDataProvider dataProvider, SingleTapClassifier singleTapClassifier, + float doubleTapSlop, long doubleTapTimeMs) { + super(dataProvider); + mSingleTapClassifier = singleTapClassifier; + mDoubleTapSlop = doubleTapSlop; + mDoubleTapTimeMs = doubleTapTimeMs; + } + + @Override + boolean isFalseTouch() { + List secondTapEvents = getRecentMotionEvents(); + Queue> historicalEvents = getHistoricalEvents(); + List firstTapEvents = historicalEvents.peek(); + + mReason = new StringBuilder(); + + if (firstTapEvents == null) { + mReason.append("Only one gesture recorded"); + return true; + } + + return !isDoubleTap(firstTapEvents, secondTapEvents, mReason); + } + + /** Returns true if the two supplied lists of {@link MotionEvent}s look like a double-tap. */ + public boolean isDoubleTap(List firstEvents, List secondEvents, + StringBuilder reason) { + + if (!mSingleTapClassifier.isTap(firstEvents)) { + reason.append("First gesture is not a tap. ").append(mSingleTapClassifier.getReason()); + return false; + } + + if (!mSingleTapClassifier.isTap(secondEvents)) { + reason.append("Second gesture is not a tap. ").append(mSingleTapClassifier.getReason()); + return false; + } + + MotionEvent firstFinalEvent = firstEvents.get(firstEvents.size() - 1); + MotionEvent secondFinalEvent = secondEvents.get(secondEvents.size() - 1); + + long dt = secondFinalEvent.getEventTime() - firstFinalEvent.getEventTime(); + + if (dt > mDoubleTapTimeMs) { + reason.append("Time between taps too large: ").append(dt).append("ms"); + return false; + } + + if (Math.abs(firstFinalEvent.getX() - secondFinalEvent.getX()) >= mDoubleTapSlop) { + reason.append("Delta X between taps too large:") + .append(Math.abs(firstFinalEvent.getX() - secondFinalEvent.getX())) + .append(" vs ") + .append(mDoubleTapSlop); + return false; + } + + if (Math.abs(firstFinalEvent.getY() - secondFinalEvent.getY()) >= mDoubleTapSlop) { + reason.append("Delta Y between taps too large:") + .append(Math.abs(firstFinalEvent.getY() - secondFinalEvent.getY())) + .append(" vs ") + .append(mDoubleTapSlop); + return false; + } + + return true; + } + + @Override + String getReason() { + return mReason.toString(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java index 85e95a66bfe3c..ed417b3d09ecf 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingClassifier.java @@ -22,6 +22,7 @@ import com.android.systemui.classifier.Classifier; import com.android.systemui.util.sensors.ProximitySensor; import java.util.List; +import java.util.Queue; /** * Base class for rules that determine False touches. @@ -30,13 +31,17 @@ abstract class FalsingClassifier { private final FalsingDataProvider mDataProvider; FalsingClassifier(FalsingDataProvider dataProvider) { - this.mDataProvider = dataProvider; + mDataProvider = dataProvider; } List getRecentMotionEvents() { return mDataProvider.getRecentMotionEvents(); } + Queue> getHistoricalEvents() { + return mDataProvider.getHistoricalMotionEvents(); + } + MotionEvent getFirstMotionEvent() { return mDataProvider.getFirstRecentMotionEvent(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java index 8d067489a8cce..4681f9709a7d1 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/FalsingDataProvider.java @@ -23,9 +23,13 @@ import android.view.MotionEvent.PointerProperties; import com.android.systemui.classifier.Classifier; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.util.time.SystemClock; import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import javax.inject.Inject; @@ -35,18 +39,21 @@ import javax.inject.Inject; public class FalsingDataProvider { private static final long MOTION_EVENT_AGE_MS = 1000; + private static final long EXTENDED_MOTION_EVENT_AGE_MS = 30 * 1000; private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI); private final int mWidthPixels; private final int mHeightPixels; private final BatteryController mBatteryController; + private final SystemClock mSystemClock; private final float mXdpi; private final float mYdpi; private @Classifier.InteractionType int mInteractionType; - private final TimeLimitedMotionEventBuffer mRecentMotionEvents = - new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); + private final Deque mExtendedMotionEvents = new LinkedList<>(); + private TimeLimitedMotionEventBuffer mRecentMotionEvents = + new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); private boolean mDirty = true; private float mAngle = 0; @@ -55,12 +62,14 @@ public class FalsingDataProvider { private MotionEvent mLastMotionEvent; @Inject - public FalsingDataProvider(DisplayMetrics displayMetrics, BatteryController batteryController) { + public FalsingDataProvider(DisplayMetrics displayMetrics, BatteryController batteryController, + SystemClock systemClock) { mXdpi = displayMetrics.xdpi; mYdpi = displayMetrics.ydpi; mWidthPixels = displayMetrics.widthPixels; mHeightPixels = displayMetrics.heightPixels; mBatteryController = batteryController; + mSystemClock = systemClock; FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi()); FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels()); @@ -81,7 +90,10 @@ public class FalsingDataProvider { } if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { - mRecentMotionEvents.clear(); + if (!mRecentMotionEvents.isEmpty()) { + mExtendedMotionEvents.addFirst(mRecentMotionEvents); + mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); + } } mRecentMotionEvents.addAll(motionEvents); @@ -112,6 +124,16 @@ public class FalsingDataProvider { return mRecentMotionEvents; } + /** Returns recent gestures, exclusive of the most recent gesture. Newer gestures come first. */ + Queue> getHistoricalMotionEvents() { + long nowMs = mSystemClock.uptimeMillis(); + + mExtendedMotionEvents.removeIf( + motionEvents -> motionEvents.isFullyExpired(nowMs - EXTENDED_MOTION_EVENT_AGE_MS)); + + return mExtendedMotionEvents; + } + /** * interactionType is defined by {@link com.android.systemui.classifier.Classifier}. */ diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/SingleTapClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/SingleTapClassifier.java new file mode 100644 index 0000000000000..47708f4dd7894 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/SingleTapClassifier.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 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 java.util.List; + +/** + * Falsing classifier that accepts or rejects a single gesture as a tap. + */ +public class SingleTapClassifier extends FalsingClassifier { + private final float mTouchSlop; + private String mReason; + + SingleTapClassifier(FalsingDataProvider dataProvider, float touchSlop) { + super(dataProvider); + mTouchSlop = touchSlop; + } + + @Override + boolean isFalseTouch() { + return !isTap(getRecentMotionEvents()); + } + + /** Given a list of {@link android.view.MotionEvent}'s, returns true if the look like a tap. */ + public boolean isTap(List motionEvents) { + float downX = motionEvents.get(0).getX(); + float downY = motionEvents.get(0).getY(); + + for (MotionEvent event : motionEvents) { + if (Math.abs(event.getX() - downX) >= mTouchSlop) { + mReason = "dX too big for a tap: " + + Math.abs(event.getX() - downX) + + "vs " + + mTouchSlop; + return false; + } else if (Math.abs(event.getY() - downY) >= mTouchSlop) { + mReason = "dY too big for a tap: " + + Math.abs(event.getY() - downY) + + "vs " + + mTouchSlop; + return false; + } + } + mReason = ""; + return true; + } + + @Override + String getReason() { + return mReason; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java index 9a83b5bd83289..92aa7c5b2f448 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBuffer.java @@ -34,12 +34,24 @@ import java.util.ListIterator; public class TimeLimitedMotionEventBuffer implements List { private final LinkedList mMotionEvents; - private long mMaxAgeMs; + private final long mMaxAgeMs; TimeLimitedMotionEventBuffer(long maxAgeMs) { super(); - this.mMaxAgeMs = maxAgeMs; - this.mMotionEvents = new LinkedList<>(); + mMaxAgeMs = maxAgeMs; + mMotionEvents = new LinkedList<>(); + } + + /** + * Returns true if the most recent event in the buffer is past the expiration time. + * + * This method does not mutate the underlying data. This method does imply that, if the supplied + * expiration time is old enough and a new {@link MotionEvent} gets added to the buffer, all + * prior events would be removed. + */ + public boolean isFullyExpired(long expirationMs) { + return mMotionEvents.isEmpty() + || mMotionEvents.getLast().getEventTime() <= expirationMs; } private void ejectOldEvents() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index 41ce51c2762f7..0b79387fac776 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -23,32 +23,44 @@ import android.view.accessibility.AccessibilityManager; import com.android.systemui.Gefingerpoken; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.statusbar.phone.DoubleTapHelper; +import com.android.systemui.statusbar.phone.NotificationTapHelper; +import com.android.systemui.util.ViewController; import javax.inject.Inject; /** * Controller for {@link ActivatableNotificationView} */ -public class ActivatableNotificationViewController { - private final ActivatableNotificationView mView; +public class ActivatableNotificationViewController + extends ViewController { private final ExpandableOutlineViewController mExpandableOutlineViewController; private final AccessibilityManager mAccessibilityManager; private final FalsingManager mFalsingManager; - private DoubleTapHelper mDoubleTapHelper; - private boolean mNeedsDimming; + private final NotificationTapHelper mNotificationTapHelper; + private final TouchHandler mTouchHandler = new TouchHandler(); - private TouchHandler mTouchHandler = new TouchHandler(); + private boolean mNeedsDimming; @Inject public ActivatableNotificationViewController(ActivatableNotificationView view, + NotificationTapHelper.Factory notificationTapHelpFactory, ExpandableOutlineViewController expandableOutlineViewController, AccessibilityManager accessibilityManager, FalsingManager falsingManager) { - mView = view; + super(view); mExpandableOutlineViewController = expandableOutlineViewController; mAccessibilityManager = accessibilityManager; mFalsingManager = falsingManager; + mNotificationTapHelper = notificationTapHelpFactory.create( + (active) -> { + if (active) { + mView.makeActive(); + mFalsingManager.onNotificationActive(); + } else { + mView.makeInactive(true /* animate */); + } + }, mView::performClick, mView::handleSlideBack); + mView.setOnActivatedListener(new ActivatableNotificationView.OnActivatedListener() { @Override public void onActivated(ActivatableNotificationView view) { @@ -64,25 +76,25 @@ public class ActivatableNotificationViewController { /** * Initialize the controller, setting up handlers and other behavior. */ - public void init() { + @Override + public void onInit() { mExpandableOutlineViewController.init(); - mDoubleTapHelper = new DoubleTapHelper(mView, (active) -> { - if (active) { - mView.makeActive(); - mFalsingManager.onNotificationActive(); - } else { - mView.makeInactive(true /* animate */); - } - }, mView::performClick, mView::handleSlideBack, - mFalsingManager::onNotificationDoubleTap); mView.setOnTouchListener(mTouchHandler); mView.setTouchHandler(mTouchHandler); - mView.setOnDimmedListener(dimmed -> { - mNeedsDimming = dimmed; - }); + mView.setOnDimmedListener(dimmed -> mNeedsDimming = dimmed); mView.setAccessibilityManager(mAccessibilityManager); } + @Override + protected void onViewAttached() { + + } + + @Override + protected void onViewDetached() { + + } + class TouchHandler implements Gefingerpoken, View.OnTouchListener { private boolean mBlockNextTouch; @@ -103,7 +115,7 @@ public class ActivatableNotificationViewController { // let's ensure we have a ripple return false; } - result = mDoubleTapHelper.onTouchEvent(ev, mView.getActualHeight()); + result = mNotificationTapHelper.onTouchEvent(ev, mView.getActualHeight()); } else { return false; } @@ -117,7 +129,7 @@ public class ActivatableNotificationViewController { && !mAccessibilityManager.isTouchExplorationEnabled()) { if (!mView.isActive()) { return true; - } else if (!mDoubleTapHelper.isWithinDoubleTapSlop(ev)) { + } else if (mFalsingManager.isFalseDoubleTap()) { mBlockNextTouch = true; mView.makeInactive(true /* animate */); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DoubleTapHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DoubleTapHelper.java deleted file mode 100644 index 78ea5c03a7b52..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DoubleTapHelper.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2017 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.statusbar.phone; - -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; - -import com.android.systemui.R; - -/** - * Detects a double tap. - */ -public class DoubleTapHelper { - - private static final long DOUBLETAP_TIMEOUT_MS = 1200; - - private final View mView; - private final ActivationListener mActivationListener; - private final DoubleTapListener mDoubleTapListener; - private final SlideBackListener mSlideBackListener; - private final DoubleTapLogListener mDoubleTapLogListener; - - private float mTouchSlop; - private float mDoubleTapSlop; - - private boolean mActivated; - - private float mDownX; - private float mDownY; - private boolean mTrackTouch; - - private float mActivationX; - private float mActivationY; - private Runnable mTapTimeoutRunnable = this::makeInactive; - - public DoubleTapHelper(View view, ActivationListener activationListener, - DoubleTapListener doubleTapListener, SlideBackListener slideBackListener, - DoubleTapLogListener doubleTapLogListener) { - mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop(); - mDoubleTapSlop = view.getResources().getDimension(R.dimen.double_tap_slop); - mView = view; - - mActivationListener = activationListener; - mDoubleTapListener = doubleTapListener; - mSlideBackListener = slideBackListener; - mDoubleTapLogListener = doubleTapLogListener; - } - - public boolean onTouchEvent(MotionEvent event) { - return onTouchEvent(event, Integer.MAX_VALUE); - } - - public boolean onTouchEvent(MotionEvent event, int maxTouchableHeight) { - int action = event.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mDownX = event.getX(); - mDownY = event.getY(); - mTrackTouch = true; - if (mDownY > maxTouchableHeight) { - mTrackTouch = false; - } - break; - case MotionEvent.ACTION_MOVE: - if (!isWithinTouchSlop(event)) { - makeInactive(); - mTrackTouch = false; - } - break; - case MotionEvent.ACTION_UP: - if (isWithinTouchSlop(event)) { - if (mSlideBackListener != null && mSlideBackListener.onSlideBack()) { - return true; - } - if (!mActivated) { - makeActive(); - mView.postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); - mActivationX = event.getX(); - mActivationY = event.getY(); - } else { - boolean withinDoubleTapSlop = isWithinDoubleTapSlop(event); - if (mDoubleTapLogListener != null) { - mDoubleTapLogListener.onDoubleTapLog(withinDoubleTapSlop, - event.getX() - mActivationX, - event.getY() - mActivationY); - } - if (withinDoubleTapSlop) { - makeInactive(); - if (!mDoubleTapListener.onDoubleTap()) { - return false; - } - } else { - makeInactive(); - mTrackTouch = false; - } - } - } else { - makeInactive(); - mTrackTouch = false; - } - break; - case MotionEvent.ACTION_CANCEL: - makeInactive(); - mTrackTouch = false; - break; - default: - break; - } - return mTrackTouch; - } - - private void makeActive() { - if (!mActivated) { - mActivated = true; - mActivationListener.onActiveChanged(true); - } - } - - private void makeInactive() { - if (mActivated) { - mActivated = false; - mActivationListener.onActiveChanged(false); - mView.removeCallbacks(mTapTimeoutRunnable); - } - } - - private boolean isWithinTouchSlop(MotionEvent event) { - return Math.abs(event.getX() - mDownX) < mTouchSlop - && Math.abs(event.getY() - mDownY) < mTouchSlop; - } - - public boolean isWithinDoubleTapSlop(MotionEvent event) { - if (!mActivated) { - // If we're not activated there's no double tap slop to satisfy. - return true; - } - - return Math.abs(event.getX() - mActivationX) < mDoubleTapSlop - && Math.abs(event.getY() - mActivationY) < mDoubleTapSlop; - } - - @FunctionalInterface - public interface ActivationListener { - void onActiveChanged(boolean active); - } - - @FunctionalInterface - public interface DoubleTapListener { - boolean onDoubleTap(); - } - - @FunctionalInterface - public interface SlideBackListener { - boolean onSlideBack(); - } - - @FunctionalInterface - public interface DoubleTapLogListener { - void onDoubleTapLog(boolean accepted, float dx, float dy); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationTapHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationTapHelper.java new file mode 100644 index 0000000000000..50c8e2e0d7101 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationTapHelper.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2017 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.statusbar.phone; + +import android.view.MotionEvent; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.util.concurrency.DelayableExecutor; + +import javax.inject.Inject; + +/** + * Detects single and double taps on notifications. + */ +public class NotificationTapHelper { + + public static final long DOUBLE_TAP_TIMEOUT_MS = 1200; + + private final ActivationListener mActivationListener; + private final DoubleTapListener mDoubleTapListener; + private final FalsingManager mFalsingManager; + private final DelayableExecutor mExecutor; + private final SlideBackListener mSlideBackListener; + + private boolean mTrackTouch; + private Runnable mTimeoutCancel; + + private NotificationTapHelper(FalsingManager falsingManager, DelayableExecutor executor, + ActivationListener activationListener, DoubleTapListener doubleTapListener, + SlideBackListener slideBackListener) { + mFalsingManager = falsingManager; + mExecutor = executor; + mActivationListener = activationListener; + mDoubleTapListener = doubleTapListener; + mSlideBackListener = slideBackListener; + } + + @VisibleForTesting + boolean onTouchEvent(MotionEvent event) { + return onTouchEvent(event, Integer.MAX_VALUE); + } + + /** Call to have the helper process a touch event. */ + public boolean onTouchEvent(MotionEvent event, int maxTouchableHeight) { + int action = event.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mTrackTouch = event.getY() <= maxTouchableHeight; + break; + case MotionEvent.ACTION_MOVE: + if (mTrackTouch && mFalsingManager.isFalseTap(false)) { + makeInactive(); + mTrackTouch = false; + } + break; + case MotionEvent.ACTION_CANCEL: + makeInactive(); + mTrackTouch = false; + break; + case MotionEvent.ACTION_UP: + mTrackTouch = false; + + // 1) See if we have confidence that we can activate after a single tap. + // 2) Else, see if it looks like a tap at all and check for a double-tap. + if (!mFalsingManager.isFalseTap(true)) { + makeInactive(); + return mDoubleTapListener.onDoubleTap(); + } else if (!mFalsingManager.isFalseTap(false)) { + if (mSlideBackListener != null && mSlideBackListener.onSlideBack()) { + return true; + } + if (mTimeoutCancel == null) { + // first tap + makeActive(); + return true; + } else { + // second tap + makeInactive(); + if (!mFalsingManager.isFalseDoubleTap()) { + return mDoubleTapListener.onDoubleTap(); + } + } + } else { + makeInactive(); + } + break; + default: + break; + } + return mTrackTouch; + } + + private void makeActive() { + mTimeoutCancel = mExecutor.executeDelayed(this::makeInactive, DOUBLE_TAP_TIMEOUT_MS); + mActivationListener.onActiveChanged(true); + } + + private void makeInactive() { + mActivationListener.onActiveChanged(false); + if (mTimeoutCancel != null) { + mTimeoutCancel.run(); + mTimeoutCancel = null; + } + } + + /** */ + @FunctionalInterface + public interface ActivationListener { + /** */ + void onActiveChanged(boolean active); + } + + /** */ + @FunctionalInterface + public interface DoubleTapListener { + /** */ + boolean onDoubleTap(); + } + + /** */ + @FunctionalInterface + public interface SlideBackListener { + /** */ + boolean onSlideBack(); + } + + /** + * Injectable factory that creates a {@link NotificationTapHelper}. + */ + public static class Factory { + private final FalsingManager mFalsingManager; + private final DelayableExecutor mDelayableExecutor; + + @Inject + public Factory(FalsingManager falsingManager, @Main DelayableExecutor delayableExecutor) { + mFalsingManager = falsingManager; + mDelayableExecutor = delayableExecutor; + } + + /** Create a {@link NotificationTapHelper} */ + public NotificationTapHelper create(ActivationListener activationListener, + DoubleTapListener doubleTapListener, SlideBackListener slideBackListener) { + return new NotificationTapHelper(mFalsingManager, mDelayableExecutor, + activationListener, doubleTapListener, slideBackListener); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerProxyTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerProxyTest.java index c3c9ecc23d598..a48bd81472b81 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerProxyTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerProxyTest.java @@ -21,9 +21,11 @@ import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHT import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertThat; +import android.content.res.Resources; import android.provider.DeviceConfig; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; +import android.view.ViewConfiguration; import androidx.test.filters.SmallTest; @@ -70,6 +72,8 @@ public class FalsingManagerProxyTest extends LeakCheckedTest { private DockManager mDockManager = new DockManagerFake(); private StatusBarStateController mStatusBarStateController = new StatusBarStateControllerImpl(new UiEventLoggerFake()); + @Mock private Resources mResources; + @Mock private ViewConfiguration mViewConfiguration; @Before public void setup() { @@ -78,7 +82,8 @@ public class FalsingManagerProxyTest extends LeakCheckedTest { mDeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, BRIGHTLINE_FALSING_MANAGER_ENABLED, "false", false); mFalsingDataProvider = new FalsingDataProvider( - new DisplayMetrics(), new FakeBatteryController(getLeakCheck())); + new DisplayMetrics(), new FakeBatteryController(getLeakCheck()), + new FakeSystemClock()); } @After @@ -92,7 +97,8 @@ public class FalsingManagerProxyTest extends LeakCheckedTest { public void test_brightLineFalsingManagerDisabled() { mProxy = new FalsingManagerProxy(getContext(), mPluginManager, mExecutor, mProximitySensor, mDeviceConfig, mDockManager, mKeyguardUpdateMonitor, - mDumpManager, mUiBgExecutor, mStatusBarStateController, mFalsingDataProvider); + mDumpManager, mUiBgExecutor, mStatusBarStateController, mResources, + mViewConfiguration, mFalsingDataProvider); assertThat(mProxy.getInternalFalsingManager(), instanceOf(FalsingManagerImpl.class)); } @@ -103,7 +109,8 @@ public class FalsingManagerProxyTest extends LeakCheckedTest { mExecutor.runAllReady(); mProxy = new FalsingManagerProxy(getContext(), mPluginManager, mExecutor, mProximitySensor, mDeviceConfig, mDockManager, mKeyguardUpdateMonitor, - mDumpManager, mUiBgExecutor, mStatusBarStateController, mFalsingDataProvider); + mDumpManager, mUiBgExecutor, mStatusBarStateController, mResources, + mViewConfiguration, mFalsingDataProvider); assertThat(mProxy.getInternalFalsingManager(), instanceOf(BrightLineFalsingManager.class)); } @@ -111,7 +118,8 @@ public class FalsingManagerProxyTest extends LeakCheckedTest { public void test_brightLineFalsingManagerToggled() throws InterruptedException { mProxy = new FalsingManagerProxy(getContext(), mPluginManager, mExecutor, mProximitySensor, mDeviceConfig, mDockManager, mKeyguardUpdateMonitor, - mDumpManager, mUiBgExecutor, mStatusBarStateController, mFalsingDataProvider); + mDumpManager, mUiBgExecutor, mStatusBarStateController, mResources, + mViewConfiguration, mFalsingDataProvider); assertThat(mProxy.getInternalFalsingManager(), instanceOf(FalsingManagerImpl.class)); mDeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/BrightLineFalsingManagerTest.java index 061664b4f6d41..30dddc02be908 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/BrightLineFalsingManagerTest.java @@ -20,14 +20,18 @@ import static org.mockito.ArgumentMatchers.any; 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.content.res.Resources; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.DisplayMetrics; +import android.view.ViewConfiguration; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.R; import com.android.systemui.dock.DockManager; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.statusbar.StatusBarState; @@ -37,6 +41,7 @@ import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.sensors.ProximitySensor; import com.android.systemui.util.sensors.ThresholdSensor; +import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.utils.leaks.FakeBatteryController; import com.android.systemui.utils.leaks.LeakCheckedTest; @@ -56,6 +61,10 @@ public class BrightLineFalsingManagerTest extends LeakCheckedTest { private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private ProximitySensor mProximitySensor; + @Mock + private Resources mResources; + @Mock + private ViewConfiguration mViewConfiguration; private SysuiStatusBarStateController mStatusBarStateController; private FalsingDataProvider mFalsingDataProvider; private FakeBatteryController mFakeBatteryController; @@ -71,14 +80,17 @@ public class BrightLineFalsingManagerTest extends LeakCheckedTest { dm.ydpi = 100; dm.widthPixels = 100; dm.heightPixels = 100; - mFalsingDataProvider = new FalsingDataProvider(dm, mFakeBatteryController); + mFalsingDataProvider = new FalsingDataProvider(dm, mFakeBatteryController, + new FakeSystemClock()); DeviceConfigProxy deviceConfigProxy = new DeviceConfigProxyFake(); DockManager dockManager = new DockManagerFake(); mStatusBarStateController = new StatusBarStateControllerImpl(new UiEventLoggerFake()); mStatusBarStateController.setState(StatusBarState.KEYGUARD); + when(mResources.getDimension(R.dimen.double_tap_slop)).thenReturn(1f); + when(mViewConfiguration.getScaledTouchSlop()).thenReturn(1); mFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, - mKeyguardUpdateMonitor, mProximitySensor, deviceConfigProxy, dockManager, - mStatusBarStateController); + mKeyguardUpdateMonitor, mProximitySensor, deviceConfigProxy, mResources, + mViewConfiguration, dockManager, mStatusBarStateController); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ClassifierTest.java index a4d198a145415..69d39fa10b72d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/ClassifierTest.java @@ -21,6 +21,7 @@ import static com.android.systemui.classifier.Classifier.UNLOCK; import android.util.DisplayMetrics; import android.view.MotionEvent; +import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.utils.leaks.FakeBatteryController; import com.android.systemui.utils.leaks.LeakCheckedTest; @@ -44,7 +45,8 @@ public class ClassifierTest extends LeakCheckedTest { displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; mFakeBatteryController = new FakeBatteryController(getLeakCheck()); - mDataProvider = new FalsingDataProvider(displayMetrics, mFakeBatteryController); + mDataProvider = new FalsingDataProvider(displayMetrics, mFakeBatteryController, + new FakeSystemClock()); mDataProvider.setInteractionType(UNLOCK); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DoubleTapClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DoubleTapClassifierTest.java new file mode 100644 index 0000000000000..9f3a1e4821c9d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/DoubleTapClassifierTest.java @@ -0,0 +1,182 @@ +/* + * 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 org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DoubleTapClassifierTest extends ClassifierTest { + + private static final int TOUCH_SLOP = 100; + private static final long DOUBLE_TAP_TIMEOUT_MS = 100; + + private List mMotionEvents = new ArrayList<>(); + private final Deque> mHistoricalMotionEvents = new LinkedList<>(); + + @Mock + private FalsingDataProvider mDataProvider; + @Mock + private SingleTapClassifier mSingleTapClassifier; + private DoubleTapClassifier mClassifier; + + @Before + public void setup() { + super.setup(); + MockitoAnnotations.initMocks(this); + mClassifier = new DoubleTapClassifier(mDataProvider, mSingleTapClassifier, TOUCH_SLOP, + DOUBLE_TAP_TIMEOUT_MS); + doReturn(mHistoricalMotionEvents).when(mDataProvider).getHistoricalMotionEvents(); + } + + @After + public void tearDown() { + for (MotionEvent motionEvent : mMotionEvents) { + motionEvent.recycle(); + } + + mMotionEvents.clear(); + super.tearDown(); + } + + + @Test + public void testSingleTap() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(true); + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, TOUCH_SLOP, 1); + + boolean result = mClassifier.isFalseTouch(); + assertThat("Single tap recognized as a valid double tap", result, is(true)); + } + + @Test + public void testDoubleTap() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(true); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + archiveMotionEvents(); + + addMotionEvent(2, 2, MotionEvent.ACTION_DOWN, TOUCH_SLOP, TOUCH_SLOP); + addMotionEvent(2, 3, MotionEvent.ACTION_UP, TOUCH_SLOP, TOUCH_SLOP); + + boolean result = mClassifier.isFalseTouch(); + assertThat(mClassifier.getReason(), result, is(false)); + } + + @Test + public void testBadFirstTap() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(false, true); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + archiveMotionEvents(); + + addMotionEvent(2, 2, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(2, 3, MotionEvent.ACTION_UP, 1, 1); + + boolean result = mClassifier.isFalseTouch(); + assertThat("Bad first touch allowed", result, is(true)); + } + + @Test + public void testBadSecondTap() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(true, false); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + archiveMotionEvents(); + + addMotionEvent(2, 2, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(2, 3, MotionEvent.ACTION_UP, 1, 1); + + boolean result = mClassifier.isFalseTouch(); + assertThat("Bad second touch allowed", result, is(true)); + } + + @Test + public void testBadTouchSlop() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(true); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + archiveMotionEvents(); + + addMotionEvent(2, 2, MotionEvent.ACTION_DOWN, TOUCH_SLOP + 1, TOUCH_SLOP); + addMotionEvent(2, 3, MotionEvent.ACTION_UP, TOUCH_SLOP, TOUCH_SLOP + 1); + + boolean result = mClassifier.isFalseTouch(); + assertThat("Sloppy second touch allowed", result, is(true)); + } + + @Test + public void testBadTouchSlow() { + when(mSingleTapClassifier.isTap(anyList())).thenReturn(true); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + archiveMotionEvents(); + + addMotionEvent(DOUBLE_TAP_TIMEOUT_MS + 1, DOUBLE_TAP_TIMEOUT_MS + 1, + MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(DOUBLE_TAP_TIMEOUT_MS + 1, DOUBLE_TAP_TIMEOUT_MS + 2, + MotionEvent.ACTION_UP, 1, 1); + + boolean result = mClassifier.isFalseTouch(); + assertThat("Slow second tap allowed", result, is(true)); + } + + private void addMotionEvent(long downMs, long eventMs, int action, int x, int y) { + MotionEvent ev = MotionEvent.obtain(downMs, eventMs, action, x, y, 0); + mMotionEvents.add(ev); + when(mDataProvider.getRecentMotionEvents()).thenReturn(mMotionEvents); + } + + private void archiveMotionEvents() { + mHistoricalMotionEvents.addFirst(mMotionEvents); + doReturn(mHistoricalMotionEvents).when(mDataProvider).getHistoricalMotionEvents(); + mMotionEvents = new ArrayList<>(); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java index f13bc7379436a..be38f44193722 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/FalsingDataProviderTest.java @@ -26,6 +26,7 @@ import android.view.MotionEvent; import androidx.test.filters.SmallTest; +import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.utils.leaks.FakeBatteryController; import org.junit.After; @@ -51,7 +52,8 @@ public class FalsingDataProviderTest extends ClassifierTest { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mFakeBatteryController); + mDataProvider = new FalsingDataProvider(displayMetrics, mFakeBatteryController, + new FakeSystemClock()); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/SingleTapClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/SingleTapClassifierTest.java new file mode 100644 index 0000000000000..642b077f0dcfe --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/SingleTapClassifierTest.java @@ -0,0 +1,159 @@ +/* + * 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 org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class SingleTapClassifierTest extends ClassifierTest { + + private static final int TOUCH_SLOP = 100; + + private final List mMotionEvents = new ArrayList<>(); + + @Mock + private FalsingDataProvider mDataProvider; + private SingleTapClassifier mClassifier; + + @Before + public void setup() { + super.setup(); + MockitoAnnotations.initMocks(this); + mClassifier = new SingleTapClassifier(mDataProvider, TOUCH_SLOP); + } + + @After + public void tearDown() { + for (MotionEvent motionEvent : mMotionEvents) { + motionEvent.recycle(); + } + + mMotionEvents.clear(); + super.tearDown(); + } + + + @Test + public void testSimpleTap_XSlop() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, TOUCH_SLOP, 1); + + assertThat(mClassifier.isFalseTouch(), is(false)); + + mMotionEvents.clear(); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, -TOUCH_SLOP + 2, 1); + + assertThat(mClassifier.isFalseTouch(), is(false)); + + } + + @Test + public void testSimpleTap_YSlop() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, TOUCH_SLOP); + + assertThat(mClassifier.isFalseTouch(), is(false)); + + mMotionEvents.clear(); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, -TOUCH_SLOP + 2); + + assertThat(mClassifier.isFalseTouch(), is(false)); + } + + + @Test + public void testFalseTap_XSlop() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, TOUCH_SLOP + 1, 1); + + assertThat(mClassifier.isFalseTouch(), is(true)); + + mMotionEvents.clear(); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, -TOUCH_SLOP - 1, 1); + + assertThat(mClassifier.isFalseTouch(), is(true)); + + } + + @Test + public void testFalseTap_YSlop() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, TOUCH_SLOP + 1); + + assertThat(mClassifier.isFalseTouch(), is(true)); + + mMotionEvents.clear(); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, -TOUCH_SLOP - 1); + + assertThat(mClassifier.isFalseTouch(), is(true)); + } + + @Test + public void testLargeMovementFalses() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_MOVE, 1, TOUCH_SLOP + 1); + addMotionEvent(0, 2, MotionEvent.ACTION_UP, 1, 1); + + assertThat(mClassifier.isFalseTouch(), is(true)); + } + + @Test + public void testDirectlySuppliedMotionEvents() { + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, 1); + + assertThat(mClassifier.isTap(mMotionEvents), is(true)); + + addMotionEvent(0, 0, MotionEvent.ACTION_DOWN, 1, 1); + addMotionEvent(0, 1, MotionEvent.ACTION_UP, 1, TOUCH_SLOP + 1); + + assertThat(mClassifier.isTap(mMotionEvents), is(false)); + + } + + private void addMotionEvent(long downMs, long eventMs, int action, int x, int y) { + MotionEvent ev = MotionEvent.obtain(downMs, eventMs, action, x, y, 0); + mMotionEvents.add(ev); + when(mDataProvider.getRecentMotionEvents()).thenReturn(mMotionEvents); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBufferTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBufferTest.java new file mode 100644 index 0000000000000..1dfffb271f023 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/brightline/TimeLimitedMotionEventBufferTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class TimeLimitedMotionEventBufferTest extends SysuiTestCase { + + private static final long MAX_AGE_MS = 100; + + private TimeLimitedMotionEventBuffer mBuffer; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mBuffer = new TimeLimitedMotionEventBuffer(MAX_AGE_MS); + } + + @After + public void tearDown() { + for (MotionEvent motionEvent : mBuffer) { + motionEvent.recycle(); + } + mBuffer.clear(); + } + + @Test + public void testAllEventsRetained() { + MotionEvent eventA = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + MotionEvent eventB = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventC = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventD = MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, 0, 0, 0); + + mBuffer.add(eventA); + mBuffer.add(eventB); + mBuffer.add(eventC); + mBuffer.add(eventD); + + assertThat(mBuffer.size(), is(4)); + } + + @Test + public void testOlderEventsRemoved() { + MotionEvent eventA = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + MotionEvent eventB = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventC = MotionEvent.obtain( + 0, MAX_AGE_MS + 1, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventD = MotionEvent.obtain( + 0, MAX_AGE_MS + 2, MotionEvent.ACTION_UP, 0, 0, 0); + + mBuffer.add(eventA); + mBuffer.add(eventB); + assertThat(mBuffer.size(), is(2)); + + mBuffer.add(eventC); + mBuffer.add(eventD); + assertThat(mBuffer.size(), is(2)); + + assertThat(mBuffer.get(0), is(eventC)); + assertThat(mBuffer.get(1), is(eventD)); + } + + @Test + public void testFullyExpired() { + MotionEvent eventA = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); + MotionEvent eventB = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventC = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 0, 0, 0); + MotionEvent eventD = MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, 0, 0, 0); + + mBuffer.add(eventA); + mBuffer.add(eventB); + mBuffer.add(eventC); + mBuffer.add(eventD); + + assertThat(mBuffer.isFullyExpired(2), is(false)); + assertThat(mBuffer.isFullyExpired(6), is(true)); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DoubleTapHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationTapHelperTest.java similarity index 69% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DoubleTapHelperTest.java rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationTapHelperTest.java index df1233af6406b..4ed27463eecd7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DoubleTapHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationTapHelperTest.java @@ -16,17 +16,12 @@ package com.android.systemui.statusbar.phone; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyFloat; -import static org.mockito.ArgumentMatchers.anyLong; 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.content.res.Resources; -import android.os.SystemClock; import android.testing.AndroidTestingRunner; import android.view.MotionEvent; import android.view.View; @@ -36,48 +31,47 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) -public class DoubleTapHelperTest extends SysuiTestCase { +public class NotificationTapHelperTest extends SysuiTestCase { - private DoubleTapHelper mDoubleTapHelper; - private int mTouchSlop; - private int mDoubleTouchSlop; + private NotificationTapHelper mNotificationTapHelper; + private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); + private final FalsingManagerFake mFalsingManager = new FalsingManagerFake(); + private final FakeExecutor mFakeExecutor = new FakeExecutor(mFakeSystemClock); @Mock private View mView; - @Mock private DoubleTapHelper.ActivationListener mActivationListener; - @Mock private DoubleTapHelper.DoubleTapListener mDoubleTapListener; - @Mock private DoubleTapHelper.SlideBackListener mSlideBackListener; - @Mock private DoubleTapHelper.DoubleTapLogListener mDoubleTapLogListener; + @Mock private NotificationTapHelper.ActivationListener mActivationListener; + @Mock private NotificationTapHelper.DoubleTapListener mDoubleTapListener; + @Mock private NotificationTapHelper.SlideBackListener mSlideBackListener; @Mock private Resources mResources; @Before public void setup() { MockitoAnnotations.initMocks(this); - mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - // The double tap slop has to be less than the regular slop, otherwise it has no effect. - mDoubleTouchSlop = mTouchSlop - 1; when(mView.getContext()).thenReturn(mContext); when(mView.getResources()).thenReturn(mResources); when(mResources.getDimension(R.dimen.double_tap_slop)) - .thenReturn((float) mDoubleTouchSlop); + .thenReturn((float) ViewConfiguration.get(mContext).getScaledTouchSlop() - 1); - mDoubleTapHelper = new DoubleTapHelper(mView, - mActivationListener, - mDoubleTapListener, - mSlideBackListener, mDoubleTapLogListener); + mFalsingManager.setFalseRobustTap(true); // Test double tapping most of the time. + + mNotificationTapHelper = new NotificationTapHelper.Factory(mFalsingManager, mFakeExecutor) + .create(mActivationListener, mDoubleTapListener, mSlideBackListener); } @Test public void testDoubleTap_success() { - long downtimeA = SystemClock.uptimeMillis(); + long downtimeA = 100; long downtimeB = downtimeA + 100; MotionEvent evDownA = MotionEvent.obtain(downtimeA, @@ -105,16 +99,13 @@ public class DoubleTapHelperTest extends SysuiTestCase { 1, 0); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); verify(mActivationListener).onActiveChanged(true); - verify(mView).postDelayed(any(Runnable.class), anyLong()); - verify(mDoubleTapLogListener, never()).onDoubleTapLog(anyBoolean(), anyFloat(), anyFloat()); verify(mDoubleTapListener, never()).onDoubleTap(); - mDoubleTapHelper.onTouchEvent(evDownB); - mDoubleTapHelper.onTouchEvent(evUpB); - verify(mDoubleTapLogListener).onDoubleTapLog(true, 0, 0); + mNotificationTapHelper.onTouchEvent(evDownB); + mNotificationTapHelper.onTouchEvent(evUpB); verify(mDoubleTapListener).onDoubleTap(); evDownA.recycle(); @@ -125,7 +116,7 @@ public class DoubleTapHelperTest extends SysuiTestCase { @Test public void testSingleTap_timeout() { - long downtimeA = SystemClock.uptimeMillis(); + long downtimeA = 100; MotionEvent evDownA = MotionEvent.obtain(downtimeA, downtimeA, @@ -140,21 +131,19 @@ public class DoubleTapHelperTest extends SysuiTestCase { 1, 0); - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); - verify(mActivationListener).onActiveChanged(true); - verify(mView).postDelayed(runnableCaptor.capture(), anyLong()); - runnableCaptor.getValue().run(); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); verify(mActivationListener).onActiveChanged(true); + drainExecutor(); + verify(mActivationListener).onActiveChanged(false); evDownA.recycle(); evUpA.recycle(); } @Test - public void testSingleTap_slop() { - long downtimeA = SystemClock.uptimeMillis(); + public void testSingleTap_falsed() { + long downtimeA = 100; MotionEvent evDownA = MotionEvent.obtain(downtimeA, downtimeA, @@ -165,13 +154,13 @@ public class DoubleTapHelperTest extends SysuiTestCase { MotionEvent evUpA = MotionEvent.obtain(downtimeA, downtimeA + 1, MotionEvent.ACTION_UP, - 1 + mTouchSlop, + 1, 1, 0); - ArgumentCaptor runnableCaptor = ArgumentCaptor.forClass(Runnable.class); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); + mFalsingManager.setFalseTap(true); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); verify(mActivationListener, never()).onActiveChanged(true); verify(mDoubleTapListener, never()).onDoubleTap(); @@ -180,8 +169,8 @@ public class DoubleTapHelperTest extends SysuiTestCase { } @Test - public void testDoubleTap_slop() { - long downtimeA = SystemClock.uptimeMillis(); + public void testDoubleTap_falsed() { + long downtimeA = 100; long downtimeB = downtimeA + 100; MotionEvent evDownA = MotionEvent.obtain(downtimeA, @@ -206,17 +195,17 @@ public class DoubleTapHelperTest extends SysuiTestCase { downtimeB + 1, MotionEvent.ACTION_UP, 1, - 1 + mDoubleTouchSlop, + 1, 0); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); - verify(mActivationListener).onActiveChanged(true); - verify(mView).postDelayed(any(Runnable.class), anyLong()); + mFalsingManager.setFalseDoubleTap(true); - mDoubleTapHelper.onTouchEvent(evDownB); - mDoubleTapHelper.onTouchEvent(evUpB); - verify(mDoubleTapLogListener).onDoubleTapLog(false, 0, mDoubleTouchSlop); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); + verify(mActivationListener).onActiveChanged(true); + + mNotificationTapHelper.onTouchEvent(evDownB); + mNotificationTapHelper.onTouchEvent(evUpB); verify(mActivationListener).onActiveChanged(false); verify(mDoubleTapListener, never()).onDoubleTap(); @@ -228,8 +217,7 @@ public class DoubleTapHelperTest extends SysuiTestCase { @Test public void testSlideBack() { - long downtimeA = SystemClock.uptimeMillis(); - long downtimeB = downtimeA + 100; + long downtimeA = 100; MotionEvent evDownA = MotionEvent.obtain(downtimeA, downtimeA, @@ -243,42 +231,24 @@ public class DoubleTapHelperTest extends SysuiTestCase { 1, 1, 0); - MotionEvent evDownB = MotionEvent.obtain(downtimeB, - downtimeB, - MotionEvent.ACTION_DOWN, - 1, - 1, - 0); - MotionEvent evUpB = MotionEvent.obtain(downtimeB, - downtimeB + 1, - MotionEvent.ACTION_UP, - 1, - 1, - 0); when(mSlideBackListener.onSlideBack()).thenReturn(true); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); - verify(mActivationListener, never()).onActiveChanged(true); - verify(mActivationListener, never()).onActiveChanged(false); - verify(mDoubleTapListener, never()).onDoubleTap(); - mDoubleTapHelper.onTouchEvent(evDownB); - mDoubleTapHelper.onTouchEvent(evUpB); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); verify(mActivationListener, never()).onActiveChanged(true); verify(mActivationListener, never()).onActiveChanged(false); verify(mDoubleTapListener, never()).onDoubleTap(); + verify(mSlideBackListener).onSlideBack(); evDownA.recycle(); evUpA.recycle(); - evDownB.recycle(); - evUpB.recycle(); } @Test public void testMoreThanTwoTaps() { - long downtimeA = SystemClock.uptimeMillis(); + long downtimeA = 100; long downtimeB = downtimeA + 100; long downtimeC = downtimeB + 100; long downtimeD = downtimeC + 100; @@ -332,33 +302,26 @@ public class DoubleTapHelperTest extends SysuiTestCase { 1, 0); - mDoubleTapHelper.onTouchEvent(evDownA); - mDoubleTapHelper.onTouchEvent(evUpA); + mNotificationTapHelper.onTouchEvent(evDownA); + mNotificationTapHelper.onTouchEvent(evUpA); verify(mActivationListener).onActiveChanged(true); - verify(mView).postDelayed(any(Runnable.class), anyLong()); - verify(mDoubleTapLogListener, never()).onDoubleTapLog(anyBoolean(), anyFloat(), anyFloat()); verify(mDoubleTapListener, never()).onDoubleTap(); - mDoubleTapHelper.onTouchEvent(evDownB); - mDoubleTapHelper.onTouchEvent(evUpB); - verify(mDoubleTapLogListener).onDoubleTapLog(true, 0, 0); + mNotificationTapHelper.onTouchEvent(evDownB); + mNotificationTapHelper.onTouchEvent(evUpB); verify(mDoubleTapListener).onDoubleTap(); reset(mView); reset(mActivationListener); - reset(mDoubleTapLogListener); reset(mDoubleTapListener); - mDoubleTapHelper.onTouchEvent(evDownC); - mDoubleTapHelper.onTouchEvent(evUpC); + mNotificationTapHelper.onTouchEvent(evDownC); + mNotificationTapHelper.onTouchEvent(evUpC); verify(mActivationListener).onActiveChanged(true); - verify(mView).postDelayed(any(Runnable.class), anyLong()); - verify(mDoubleTapLogListener, never()).onDoubleTapLog(anyBoolean(), anyFloat(), anyFloat()); verify(mDoubleTapListener, never()).onDoubleTap(); - mDoubleTapHelper.onTouchEvent(evDownD); - mDoubleTapHelper.onTouchEvent(evUpD); - verify(mDoubleTapLogListener).onDoubleTapLog(true, 0, 0); + mNotificationTapHelper.onTouchEvent(evDownD); + mNotificationTapHelper.onTouchEvent(evUpD); verify(mDoubleTapListener).onDoubleTap(); evDownA.recycle(); @@ -366,4 +329,9 @@ public class DoubleTapHelperTest extends SysuiTestCase { evDownB.recycle(); evUpB.recycle(); } + + private void drainExecutor() { + mFakeExecutor.advanceClockToLast(); + mFakeExecutor.runAllReady(); + } }