Merge "Add Tap falsing to the FalsingManager."
This commit is contained in:
committed by
Android (Google) Code Review
commit
95c6057dfd
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user