Merge "Add Tap falsing to the FalsingManager."

This commit is contained in:
TreeHugger Robot
2020-11-19 15:42:26 +00:00
committed by Android (Google) Code Review
21 changed files with 1099 additions and 310 deletions

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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();

View File

@@ -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

View File

@@ -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<MotionEvent> secondTapEvents = getRecentMotionEvents();
Queue<? extends List<MotionEvent>> historicalEvents = getHistoricalEvents();
List<MotionEvent> 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<MotionEvent> firstEvents, List<MotionEvent> 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();
}
}

View File

@@ -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<MotionEvent> getRecentMotionEvents() {
return mDataProvider.getRecentMotionEvents();
}
Queue<? extends List<MotionEvent>> getHistoricalEvents() {
return mDataProvider.getHistoricalMotionEvents();
}
MotionEvent getFirstMotionEvent() {
return mDataProvider.getFirstRecentMotionEvent();
}

View File

@@ -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<TimeLimitedMotionEventBuffer> 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<? extends List<MotionEvent>> 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}.
*/

View File

@@ -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<MotionEvent> 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;
}
}

View File

@@ -34,12 +34,24 @@ import java.util.ListIterator;
public class TimeLimitedMotionEventBuffer implements List<MotionEvent> {
private final LinkedList<MotionEvent> 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() {

View File

@@ -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<ActivatableNotificationView> {
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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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<MotionEvent> mMotionEvents = new ArrayList<>();
private final Deque<List<MotionEvent>> 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<>();
}
}

View File

@@ -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

View File

@@ -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<MotionEvent> 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);
}
}

View File

@@ -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));
}
}

View File

@@ -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<Runnable> 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<Runnable> 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();
}
}