summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/answer/impl/classifier
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/incallui/answer/impl/classifier')
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java99
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java193
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Classifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ClassifierData.java96
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java37
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java43
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/FalsingManager.java140
-rw-r--r--java/com/android/incallui/answer/impl/classifier/GestureClassifier.java31
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java115
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java142
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java45
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Point.java95
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java51
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java97
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java28
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java147
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java40
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Stroke.java72
-rw-r--r--java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java28
33 files changed, 2032 insertions, 0 deletions
diff --git a/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
new file mode 100644
index 000000000..ac504444e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.Map;
+
+/**
+ * A classifier which looks at the speed and distance between successive points of a Stroke. It
+ * looks at two consecutive speeds between two points and calculates the ratio between them. The
+ * final result is the maximum of these values. It does the same for distances. If some speed or
+ * distance is equal to zero then the ratio between this and the next part is not calculated. To the
+ * duration of each part there is added one nanosecond so that it is always possible to calculate
+ * the speed of a part.
+ */
+class AccelerationClassifier extends StrokeClassifier {
+ private final Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AccelerationClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ACC";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+ Point point = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data(point));
+ } else {
+ mStrokeMap.get(stroke).addPoint(point);
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return 2 * SpeedRatioEvaluator.evaluate(data.maxSpeedRatio);
+ }
+
+ private static class Data {
+
+ static final float MILLIS_TO_NANOS = 1e6f;
+
+ Point previousPoint;
+ float previousSpeed = 0;
+ float maxSpeedRatio = 0;
+
+ public Data(Point point) {
+ previousPoint = point;
+ }
+
+ public void addPoint(Point point) {
+ float distance = previousPoint.dist(point);
+ float duration = (float) (point.timeOffsetNano - previousPoint.timeOffsetNano + 1);
+ float speed = distance / duration;
+
+ if (duration > 20 * MILLIS_TO_NANOS || duration < 5 * MILLIS_TO_NANOS) {
+ // reject this segment and ensure we won't use data about it in the next round.
+ previousSpeed = 0;
+ previousPoint = point;
+ return;
+ }
+ if (previousSpeed != 0.0f) {
+ maxSpeedRatio = Math.max(maxSpeedRatio, speed / previousSpeed);
+ }
+
+ previousSpeed = speed;
+ previousPoint = point;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
new file mode 100644
index 000000000..dbfbcfc1c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which calculates the variance of differences between successive angles in a stroke.
+ * For each stroke it keeps its last three points. If some successive points are the same, it
+ * ignores the repetitions. If a new point is added, the classifier calculates the angle between the
+ * last three points. After that, it calculates the difference between this angle and the previously
+ * calculated angle. Then it calculates the variance of the differences from a stroke. To the
+ * differences there is artificially added value 0.0 and the difference between the first angle and
+ * PI (angles are in radians). It helps with strokes which have few points and punishes more strokes
+ * which are not smooth.
+ *
+ * <p>This classifier also tries to split the stroke into two parts in the place in which the
+ * biggest angle is. It calculates the angle variance of the two parts and sums them up. The reason
+ * the classifier is doing this, is because some human swipes at the beginning go for a moment in
+ * one direction and then they rapidly change direction for the rest of the stroke (like a tick).
+ * The final result is the minimum of angle variance of the whole stroke and the sum of angle
+ * variances of the two parts split up. The classifier tries the tick option only if the first part
+ * is shorter than the second part.
+ *
+ * <p>Additionally, the classifier classifies the angles as left angles (those angles which value is
+ * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles ([PI - ANGLE_DEVIATION, PI +
+ * ANGLE_DEVIATION] interval) and right angles ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then
+ * calculates the percentage of angles which are in the same direction (straight angles can be left
+ * angels or right angles)
+ */
+class AnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 20.0f;
+ private static final float MIN_MOVE_DIST_DP = .01f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private float mFirstAngleVariance;
+ private float mPreviousAngle;
+ private float mBiggestAngle;
+ private float mSumSquares;
+ private float mSecondSumSquares;
+ private float mSum;
+ private float mSecondSum;
+ private float mCount;
+ private float mSecondCount;
+ private float mFirstLength;
+ private float mLength;
+ private float mAnglesCount;
+ private float mLeftAngles;
+ private float mRightAngles;
+ private float mStraightAngles;
+
+ public Data() {
+ mFirstAngleVariance = 0.0f;
+ mPreviousAngle = (float) Math.PI;
+ mBiggestAngle = 0.0f;
+ mSumSquares = mSecondSumSquares = 0.0f;
+ mSum = mSecondSum = 0.0f;
+ mCount = mSecondCount = 1.0f;
+ mLength = mFirstLength = 0.0f;
+ mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ // Checking if the added point is different than the previously added point
+ // Repetitions and short distances are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || (!mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)
+ && (mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point)
+ > MIN_MOVE_DIST_DP))) {
+ if (!mLastThreePoints.isEmpty()) {
+ mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point);
+ }
+ mLastThreePoints.add(point);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle < Math.PI - ANGLE_DEVIATION) {
+ mLeftAngles++;
+ } else if (angle <= Math.PI + ANGLE_DEVIATION) {
+ mStraightAngles++;
+ } else {
+ mRightAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+
+ // If this is the biggest angle of the stroke so then we save the value of
+ // the angle variance so far and start to count the values for the angle
+ // variance of the second part.
+ if (mBiggestAngle < angle) {
+ mBiggestAngle = angle;
+ mFirstLength = mLength;
+ mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ mSecondSumSquares = 0.0f;
+ mSecondSum = 0.0f;
+ mSecondCount = 1.0f;
+ } else {
+ mSecondSum += difference;
+ mSecondSumSquares += difference * difference;
+ mSecondCount += 1.0f;
+ }
+
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance(float sumSquares, float sum, float count) {
+ return sumSquares / count - (sum / count) * (sum / count);
+ }
+
+ public float getAnglesVariance() {
+ float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ if (mFirstLength < mLength / 2f) {
+ anglesVariance =
+ Math.min(
+ anglesVariance,
+ mFirstAngleVariance
+ + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
+ }
+ return anglesVariance;
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
new file mode 100644
index 000000000..49a183596
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class AnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
new file mode 100644
index 000000000..db4de6a3b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class AnglesVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.05) {
+ evaluation++;
+ }
+ if (value > 0.10) {
+ evaluation++;
+ }
+ if (value > 0.20) {
+ evaluation++;
+ }
+ if (value > 0.40) {
+ evaluation++;
+ }
+ if (value > 0.80) {
+ evaluation++;
+ }
+ if (value > 1.50) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Classifier.java b/java/com/android/incallui/answer/impl/classifier/Classifier.java
new file mode 100644
index 000000000..c6fbff327
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Classifier.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+/** An abstract class for classifiers for touch and sensor events. */
+abstract class Classifier {
+
+ /** Contains all the information about touch events from which the classifier can query */
+ protected ClassifierData mClassifierData;
+
+ /** Informs the classifier that a new touch event has occurred */
+ public void onTouchEvent(MotionEvent event) {}
+
+ /** Informs the classifier that a sensor change occurred */
+ public void onSensorChanged(SensorEvent event) {}
+
+ public abstract String getTag();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ClassifierData.java b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
new file mode 100644
index 000000000..ae07d27a0
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Contains data which is used to classify interaction sequences on the lockscreen. It does, for
+ * example, provide information on the current touch state.
+ */
+class ClassifierData {
+ private SparseArray<Stroke> mCurrentStrokes = new SparseArray<>();
+ private ArrayList<Stroke> mEndingStrokes = new ArrayList<>();
+ private final float mDpi;
+ private final float mScreenHeight;
+
+ public ClassifierData(float dpi, float screenHeight) {
+ mDpi = dpi;
+ mScreenHeight = screenHeight / dpi;
+ }
+
+ public void update(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCurrentStrokes.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (mCurrentStrokes.get(id) == null) {
+ // TODO (keyboardr): See if there's a way to use event.getEventTimeNanos() instead
+ mCurrentStrokes.put(
+ id, new Stroke(TimeUnit.MILLISECONDS.toNanos(event.getEventTime()), mDpi));
+ }
+ mCurrentStrokes
+ .get(id)
+ .addPoint(
+ event.getX(i), event.getY(i), TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mEndingStrokes.add(getStroke(id));
+ }
+ }
+ }
+
+ void cleanUp(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mCurrentStrokes.remove(id);
+ }
+ }
+ }
+
+ /** @return the list of Strokes which are ending in the recently added MotionEvent */
+ public ArrayList<Stroke> getEndingStrokes() {
+ return mEndingStrokes;
+ }
+
+ /**
+ * @param id the id from MotionEvent
+ * @return the Stroke assigned to the id
+ */
+ public Stroke getStroke(int id) {
+ return mCurrentStrokes.get(id);
+ }
+
+ /** @return the height of the screen in inches */
+ public float getScreenHeight() {
+ return mScreenHeight;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
new file mode 100644
index 000000000..068626859
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the general direction of a stroke and evaluates it depending on the
+ * type of action that takes place.
+ */
+public class DirectionClassifier extends StrokeClassifier {
+ public DirectionClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DIR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Point firstPoint = stroke.getPoints().get(0);
+ Point lastPoint = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ return DirectionEvaluator.evaluate(lastPoint.x - firstPoint.x, lastPoint.y - firstPoint.y);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
new file mode 100644
index 000000000..cdc1cfe1e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class DirectionEvaluator {
+ public static float evaluate(float xDiff, float yDiff) {
+ return Math.abs(yDiff) < Math.abs(xDiff) ? 5.5f : 0.0f;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
new file mode 100644
index 000000000..0b9f1138d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the duration of the stroke and its number of
+ * points.
+ */
+class DurationCountClassifier extends StrokeClassifier {
+ public DurationCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DUR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return DurationCountEvaluator.evaluate(stroke.getDurationSeconds() / stroke.getCount());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
new file mode 100644
index 000000000..5b232fe95
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class DurationCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.0105) {
+ evaluation++;
+ }
+ if (value < 0.00909) {
+ evaluation++;
+ }
+ if (value < 0.00667) {
+ evaluation++;
+ }
+ if (value > 0.0333) {
+ evaluation++;
+ }
+ if (value > 0.0500) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java
new file mode 100644
index 000000000..95b317638
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the distance between the first and the last point from the stroke.
+ */
+class EndPointLengthClassifier extends StrokeClassifier {
+ public EndPointLengthClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_LNGTH";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return EndPointLengthEvaluator.evaluate(stroke.getEndPointLength());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
new file mode 100644
index 000000000..74bfffba4
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class EndPointLengthEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.05) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.1) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.2) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.3) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.4) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.5) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
new file mode 100644
index 000000000..01a35c126
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the total length covered by the stroke and the
+ * distance between the first and last point from this stroke.
+ */
+class EndPointRatioClassifier extends StrokeClassifier {
+ public EndPointRatioClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_RTIO";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float ratio;
+ if (stroke.getTotalLength() == 0.0f) {
+ ratio = 1.0f;
+ } else {
+ ratio = stroke.getEndPointLength() / stroke.getTotalLength();
+ }
+ return EndPointRatioEvaluator.evaluate(ratio);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
new file mode 100644
index 000000000..1d64bea8e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class EndPointRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.85) {
+ evaluation++;
+ }
+ if (value < 0.75) {
+ evaluation++;
+ }
+ if (value < 0.65) {
+ evaluation++;
+ }
+ if (value < 0.55) {
+ evaluation++;
+ }
+ if (value < 0.45) {
+ evaluation++;
+ }
+ if (value < 0.35) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/FalsingManager.java b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
new file mode 100644
index 000000000..fdcc0a3f9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.PowerManager;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * When the phone is locked, listens to touch, sensor and phone events and sends them to
+ * HumanInteractionClassifier to determine if touches are coming from a human.
+ */
+public class FalsingManager implements SensorEventListener {
+ private static final int[] CLASSIFIER_SENSORS =
+ new int[] {
+ Sensor.TYPE_PROXIMITY,
+ };
+
+ private final SensorManager mSensorManager;
+ private final HumanInteractionClassifier mHumanInteractionClassifier;
+ private final AccessibilityManager mAccessibilityManager;
+
+ private boolean mSessionActive = false;
+ private boolean mScreenOn;
+
+ public FalsingManager(Context context) {
+ mSensorManager = context.getSystemService(SensorManager.class);
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mHumanInteractionClassifier = new HumanInteractionClassifier(context);
+ mScreenOn = context.getSystemService(PowerManager.class).isInteractive();
+ }
+
+ /** Returns {@code true} iff the FalsingManager is enabled and able to classify touches */
+ public boolean isEnabled() {
+ return mHumanInteractionClassifier.isEnabled();
+ }
+
+ /**
+ * Returns {@code true} iff the classifier determined that this is not a human interacting with
+ * the phone.
+ */
+ public boolean isFalseTouch() {
+ // Touch exploration triggers false positives in the classifier and
+ // already sufficiently prevents false unlocks.
+ return !mAccessibilityManager.isTouchExplorationEnabled()
+ && mHumanInteractionClassifier.isFalseTouch();
+ }
+
+ /**
+ * Should be called when the screen turns on and the related Views become visible. This will start
+ * tracking changes if the manager is enabled.
+ */
+ public void onScreenOn() {
+ mScreenOn = true;
+ sessionEntrypoint();
+ }
+
+ /**
+ * Should be called when the screen turns off or the related Views are no longer visible. This
+ * will cause the manager to stop tracking changes.
+ */
+ public void onScreenOff() {
+ mScreenOn = false;
+ sessionExitpoint();
+ }
+
+ /**
+ * Should be called when a new touch event has been received and should be classified.
+ *
+ * @param event MotionEvent to be classified as human or false.
+ */
+ public void onTouchEvent(MotionEvent event) {
+ if (mSessionActive) {
+ mHumanInteractionClassifier.onTouchEvent(event);
+ }
+ }
+
+ @Override
+ public synchronized void onSensorChanged(SensorEvent event) {
+ mHumanInteractionClassifier.onSensorChanged(event);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+
+ private boolean shouldSessionBeActive() {
+ return isEnabled() && mScreenOn;
+ }
+
+ private boolean sessionEntrypoint() {
+ if (!mSessionActive && shouldSessionBeActive()) {
+ onSessionStart();
+ return true;
+ }
+ return false;
+ }
+
+ private void sessionExitpoint() {
+ if (mSessionActive && !shouldSessionBeActive()) {
+ mSessionActive = false;
+ mSensorManager.unregisterListener(this);
+ }
+ }
+
+ private void onSessionStart() {
+ mSessionActive = true;
+
+ if (mHumanInteractionClassifier.isEnabled()) {
+ registerSensors(CLASSIFIER_SENSORS);
+ }
+ }
+
+ private void registerSensors(int[] sensors) {
+ for (int sensorType : sensors) {
+ Sensor s = mSensorManager.getDefaultSensor(sensorType);
+ if (s != null) {
+ mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
new file mode 100644
index 000000000..afd7ea0e7
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * An abstract class for classifiers which classify the whole gesture (all the strokes which
+ * occurred from DOWN event to UP/CANCEL event)
+ */
+abstract class GestureClassifier extends Classifier {
+
+ /**
+ * @return a non-negative value which is used to determine whether the most recent gesture is a
+ * false interaction; the bigger the value the greater the chance that this a false
+ * interaction.
+ */
+ public abstract float getFalseTouchEvaluation();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
new file mode 100644
index 000000000..3f302c65f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+
+/**
+ * Holds the evaluations for ended strokes and gestures. These values are decreased through time.
+ */
+class HistoryEvaluator {
+ private static final float INTERVAL = 50.0f;
+ private static final float HISTORY_FACTOR = 0.9f;
+ private static final float EPSILON = 1e-5f;
+
+ private final ArrayList<Data> mStrokes = new ArrayList<>();
+ private final ArrayList<Data> mGestureWeights = new ArrayList<>();
+ private long mLastUpdate;
+
+ public HistoryEvaluator() {
+ mLastUpdate = SystemClock.elapsedRealtime();
+ }
+
+ public void addStroke(float evaluation) {
+ decayValue();
+ mStrokes.add(new Data(evaluation));
+ }
+
+ public void addGesture(float evaluation) {
+ decayValue();
+ mGestureWeights.add(new Data(evaluation));
+ }
+
+ /** Calculates the weighted average of strokes and adds to it the weighted average of gestures */
+ public float getEvaluation() {
+ return weightedAverage(mStrokes) + weightedAverage(mGestureWeights);
+ }
+
+ private float weightedAverage(ArrayList<Data> list) {
+ float sumValue = 0.0f;
+ float sumWeight = 0.0f;
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ Data data = list.get(i);
+ sumValue += data.evaluation * data.weight;
+ sumWeight += data.weight;
+ }
+
+ if (sumWeight == 0.0f) {
+ return 0.0f;
+ }
+
+ return sumValue / sumWeight;
+ }
+
+ private void decayValue() {
+ long time = SystemClock.elapsedRealtime();
+
+ if (time <= mLastUpdate) {
+ return;
+ }
+
+ // All weights are multiplied by HISTORY_FACTOR after each INTERVAL milliseconds.
+ float factor = (float) Math.pow(HISTORY_FACTOR, (time - mLastUpdate) / INTERVAL);
+
+ decayValue(mStrokes, factor);
+ decayValue(mGestureWeights, factor);
+ mLastUpdate = time;
+ }
+
+ private void decayValue(ArrayList<Data> list, float factor) {
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ list.get(i).weight *= factor;
+ }
+
+ // Removing evaluations with such small weights that they do not matter anymore
+ while (!list.isEmpty() && isZero(list.get(0).weight)) {
+ list.remove(0);
+ }
+ }
+
+ private boolean isZero(float x) {
+ return x <= EPSILON && x >= -EPSILON;
+ }
+
+ /**
+ * For each stroke it holds its initial value and the current weight. Initially the weight is set
+ * to 1.0
+ */
+ private static class Data {
+ public float evaluation;
+ public float weight;
+
+ public Data(float evaluation) {
+ this.evaluation = evaluation;
+ weight = 1.0f;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
new file mode 100644
index 000000000..1d3d7ef22
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.SensorEvent;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** An classifier trying to determine whether it is a human interacting with the phone or not. */
+class HumanInteractionClassifier extends Classifier {
+
+ private static final String CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED =
+ "answer_false_touch_detection_enabled";
+
+ private final StrokeClassifier[] mStrokeClassifiers;
+ private final GestureClassifier[] mGestureClassifiers;
+ private final HistoryEvaluator mHistoryEvaluator;
+ private final boolean mEnabled;
+
+ HumanInteractionClassifier(Context context) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+
+ // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi
+ // were to be used separately. Due negligible differences in xdpi and ydpi we can just
+ // take the average.
+ // Note that xdpi and ydpi are the physical pixels per inch and are not affected by scaling.
+ float dpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f;
+ mClassifierData = new ClassifierData(dpi, displayMetrics.heightPixels);
+ mHistoryEvaluator = new HistoryEvaluator();
+ mEnabled =
+ ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED, true);
+
+ mStrokeClassifiers =
+ new StrokeClassifier[] {
+ new AnglesClassifier(mClassifierData),
+ new SpeedClassifier(mClassifierData),
+ new DurationCountClassifier(mClassifierData),
+ new EndPointRatioClassifier(mClassifierData),
+ new EndPointLengthClassifier(mClassifierData),
+ new AccelerationClassifier(mClassifierData),
+ new SpeedAnglesClassifier(mClassifierData),
+ new LengthCountClassifier(mClassifierData),
+ new DirectionClassifier(mClassifierData)
+ };
+
+ mGestureClassifiers =
+ new GestureClassifier[] {
+ new PointerCountClassifier(mClassifierData), new ProximityClassifier(mClassifierData)
+ };
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+
+ // If the user is dragging down the notification, they might want to drag it down
+ // enough to see the content, read it for a while and then lift the finger to open
+ // the notification. This kind of motion scores very bad in the Classifier so the
+ // MotionEvents which are close to the current position of the finger are not
+ // sent to the classifiers until the finger moves far enough. When the finger if lifted
+ // up, the last MotionEvent which was far enough from the finger is set as the final
+ // MotionEvent and sent to the Classifiers.
+ addTouchEvent(event);
+ }
+
+ private void addTouchEvent(MotionEvent event) {
+ mClassifierData.update(event);
+
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ for (GestureClassifier c : mGestureClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ int size = mClassifierData.getEndingStrokes().size();
+ for (int i = 0; i < size; i++) {
+ Stroke stroke = mClassifierData.getEndingStrokes().get(i);
+ float evaluation = 0.0f;
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ float e = c.getFalseTouchEvaluation(stroke);
+ evaluation += e;
+ }
+
+ mHistoryEvaluator.addStroke(evaluation);
+ }
+
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ float evaluation = 0.0f;
+ for (GestureClassifier c : mGestureClassifiers) {
+ float e = c.getFalseTouchEvaluation();
+ evaluation += e;
+ }
+ mHistoryEvaluator.addGesture(evaluation);
+ }
+
+ mClassifierData.cleanUp(event);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ for (Classifier c : mStrokeClassifiers) {
+ c.onSensorChanged(event);
+ }
+
+ for (Classifier c : mGestureClassifiers) {
+ c.onSensorChanged(event);
+ }
+ }
+
+ boolean isFalseTouch() {
+ float evaluation = mHistoryEvaluator.getEvaluation();
+ return evaluation >= 5.0f;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public String getTag() {
+ return "HIC";
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
new file mode 100644
index 000000000..7dd2ab674
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ * The number of points is subtracted by 2 because the UP event comes in with some delay and it
+ * should not influence the ratio and also strokes which are long and have a small number of points
+ * are punished more (these kind of strokes are usually bad ones and they tend to score well in
+ * other classifiers).
+ */
+class LengthCountClassifier extends StrokeClassifier {
+ public LengthCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "LEN_CNT";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return LengthCountEvaluator.evaluate(
+ stroke.getTotalLength() / Math.max(1.0f, stroke.getCount() - 2));
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
new file mode 100644
index 000000000..2a2225a00
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ */
+class LengthCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.09) {
+ evaluation++;
+ }
+ if (value < 0.05) {
+ evaluation++;
+ }
+ if (value < 0.02) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ if (value > 0.9) {
+ evaluation++;
+ }
+ if (value > 1.2) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Point.java b/java/com/android/incallui/answer/impl/classifier/Point.java
new file mode 100644
index 000000000..5ea48b4ce
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Point.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class Point {
+ public float x;
+ public float y;
+ public long timeOffsetNano;
+
+ public Point(float x, float y) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = 0;
+ }
+
+ public Point(float x, float y, long timeOffsetNano) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = timeOffsetNano;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Point)) {
+ return false;
+ }
+ Point otherPoint = ((Point) other);
+ return x == otherPoint.x && y == otherPoint.y;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
+ result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
+ return result;
+ }
+
+ public float dist(Point a) {
+ return (float) Math.hypot(a.x - x, a.y - y);
+ }
+
+ /**
+ * Calculates the cross product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float crossProduct(Point a, Point b) {
+ return (a.x - x) * (b.y - y) - (a.y - y) * (b.x - x);
+ }
+
+ /**
+ * Calculates the dot product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float dotProduct(Point a, Point b) {
+ return (a.x - x) * (b.x - x) + (a.y - y) * (b.y - y);
+ }
+
+ /**
+ * Calculates the angle in radians created by points (a, this, b). If any two of these points are
+ * the same, the method will return 0.0f
+ *
+ * @return the angle in radians
+ */
+ public float getAngle(Point a, Point b) {
+ float dist1 = dist(a);
+ float dist2 = dist(b);
+
+ if (dist1 == 0.0f || dist2 == 0.0f) {
+ return 0.0f;
+ }
+
+ float crossProduct = crossProduct(a, b);
+ float dotProduct = dotProduct(a, b);
+ float cos = Math.min(1.0f, Math.max(-1.0f, dotProduct / dist1 / dist2));
+ float angle = (float) Math.acos(cos);
+ if (crossProduct < 0.0) {
+ angle = 2.0f * (float) Math.PI - angle;
+ }
+ return angle;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java
new file mode 100644
index 000000000..070de6c9b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.view.MotionEvent;
+
+/** A classifier which looks at the total number of traces in the whole gesture. */
+class PointerCountClassifier extends GestureClassifier {
+ private int mCount;
+
+ public PointerCountClassifier(ClassifierData classifierData) {
+ mCount = 0;
+ }
+
+ @Override
+ public String getTag() {
+ return "PTR_CNT";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCount = 1;
+ }
+
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ ++mCount;
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return PointerCountEvaluator.evaluate(mCount);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
new file mode 100644
index 000000000..aa972da8c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class PointerCountEvaluator {
+ public static float evaluate(int value) {
+ return (value - 1) * (value - 1);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java
new file mode 100644
index 000000000..28701ea6d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A classifier which looks at the proximity sensor during the gesture. It calculates the percentage
+ * the proximity sensor showing the near state during the whole gesture
+ */
+class ProximityClassifier extends GestureClassifier {
+ private long mGestureStartTimeNano;
+ private long mNearStartTimeNano;
+ private long mNearDuration;
+ private boolean mNear;
+ private float mAverageNear;
+
+ public ProximityClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "PROX";
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+ update(event.values[0] < event.sensor.getMaximumRange(), event.timestamp);
+ }
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mGestureStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearDuration = 0;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ update(mNear, TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+ long duration = TimeUnit.MILLISECONDS.toNanos(event.getEventTime()) - mGestureStartTimeNano;
+
+ if (duration == 0) {
+ mAverageNear = mNear ? 1.0f : 0.0f;
+ } else {
+ mAverageNear = (float) mNearDuration / (float) duration;
+ }
+ }
+ }
+
+ /**
+ * @param near is the sensor showing the near state right now
+ * @param timestampNano time of this event in nanoseconds
+ */
+ private void update(boolean near, long timestampNano) {
+ // This if is necessary because MotionEvents and SensorEvents do not come in
+ // chronological order
+ if (timestampNano > mNearStartTimeNano) {
+ // if the state before was near then add the difference of the current time and
+ // mNearStartTimeNano to mNearDuration.
+ if (mNear) {
+ mNearDuration += timestampNano - mNearStartTimeNano;
+ }
+
+ // if the new state is near, set mNearStartTimeNano equal to this moment.
+ if (near) {
+ mNearStartTimeNano = timestampNano;
+ }
+ }
+ mNear = near;
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return ProximityEvaluator.evaluate(mAverageNear);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
new file mode 100644
index 000000000..14636c644
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class ProximityEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ float threshold = 0.1f;
+ if (value >= threshold) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
new file mode 100644
index 000000000..36ae3ad7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which for each point from a stroke, it creates a point on plane with coordinates
+ * (timeOffsetNano, distanceCoveredUpToThisPoint) (scaled by DURATION_SCALE and LENGTH_SCALE) and
+ * then it calculates the angle variance of these points like the class {@link AnglesClassifier}
+ * (without splitting it into two parts). The classifier ignores the last point of a stroke because
+ * the UP event comes in with some delay and this ruins the smoothness of this curve. Additionally,
+ * the classifier classifies calculates the percentage of angles which value is in [PI -
+ * ANGLE_DEVIATION, 2* PI) interval. The reason why the classifier does that is because the speed of
+ * a good stroke is most often increases, so most of these angels should be in this interval.
+ */
+class SpeedAnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public SpeedAnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "SPD_ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+
+ if (action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL
+ && !(action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return SpeedVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + SpeedAnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float DURATION_SCALE = 1e8f;
+ private static final float LENGTH_SCALE = 1.0f;
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 10.0f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private Point mPreviousPoint;
+ private float mPreviousAngle;
+ private float mSumSquares;
+ private float mSum;
+ private float mCount;
+ private float mDist;
+ private float mAnglesCount;
+ private float mAcceleratingAngles;
+
+ public Data() {
+ mPreviousPoint = null;
+ mPreviousAngle = (float) Math.PI;
+ mSumSquares = 0.0f;
+ mSum = 0.0f;
+ mCount = 1.0f;
+ mDist = 0.0f;
+ mAnglesCount = mAcceleratingAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ if (mPreviousPoint != null) {
+ mDist += mPreviousPoint.dist(point);
+ }
+
+ mPreviousPoint = point;
+ Point speedPoint =
+ new Point((float) point.timeOffsetNano / DURATION_SCALE, mDist / LENGTH_SCALE);
+
+ // Checking if the added point is different than the previously added point
+ // Repetitions are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(speedPoint)) {
+ mLastThreePoints.add(speedPoint);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle >= (float) Math.PI - ANGLE_DEVIATION) {
+ mAcceleratingAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance() {
+ return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (mAcceleratingAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
new file mode 100644
index 000000000..5a8bc3556
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class SpeedAnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
new file mode 100644
index 000000000..f3ade3f49
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier that looks at the speed of the stroke. It calculates the speed of a stroke in inches
+ * per second.
+ */
+class SpeedClassifier extends StrokeClassifier {
+
+ public SpeedClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "SPD";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float duration = stroke.getDurationSeconds();
+ if (duration == 0.0f) {
+ return SpeedEvaluator.evaluate(0.0f);
+ }
+ return SpeedEvaluator.evaluate(stroke.getTotalLength() / duration);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java
new file mode 100644
index 000000000..4f9aace0e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class SpeedEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 4.0) {
+ evaluation++;
+ }
+ if (value < 2.2) {
+ evaluation++;
+ }
+ if (value > 35.0) {
+ evaluation++;
+ }
+ if (value > 50.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
new file mode 100644
index 000000000..7ae111313
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class SpeedRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value == 0) {
+ return 0;
+ }
+ if (value <= 1.0) {
+ evaluation++;
+ }
+ if (value <= 0.5) {
+ evaluation++;
+ }
+ if (value > 9.0) {
+ evaluation++;
+ }
+ if (value > 18.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java
new file mode 100644
index 000000000..211650cbb
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+class SpeedVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.06) {
+ evaluation++;
+ }
+ if (value > 0.15) {
+ evaluation++;
+ }
+ if (value > 0.3) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Stroke.java b/java/com/android/incallui/answer/impl/classifier/Stroke.java
new file mode 100644
index 000000000..c542d0f7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Stroke.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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.incallui.answer.impl.classifier;
+
+import java.util.ArrayList;
+
+/**
+ * Contains data about a stroke (a single trace, all the events from a given id from the
+ * DOWN/POINTER_DOWN event till the UP/POINTER_UP/CANCEL event.)
+ */
+class Stroke {
+
+ private static final float NANOS_TO_SECONDS = 1e9f;
+
+ private ArrayList<Point> mPoints = new ArrayList<>();
+ private long mStartTimeNano;
+ private long mEndTimeNano;
+ private float mLength;
+ private final float mDpi;
+
+ public Stroke(long eventTimeNano, float dpi) {
+ mDpi = dpi;
+ mStartTimeNano = mEndTimeNano = eventTimeNano;
+ }
+
+ public void addPoint(float x, float y, long eventTimeNano) {
+ mEndTimeNano = eventTimeNano;
+ Point point = new Point(x / mDpi, y / mDpi, eventTimeNano - mStartTimeNano);
+ if (!mPoints.isEmpty()) {
+ mLength += mPoints.get(mPoints.size() - 1).dist(point);
+ }
+ mPoints.add(point);
+ }
+
+ public int getCount() {
+ return mPoints.size();
+ }
+
+ public float getTotalLength() {
+ return mLength;
+ }
+
+ public float getEndPointLength() {
+ return mPoints.get(0).dist(mPoints.get(mPoints.size() - 1));
+ }
+
+ public long getDurationNanos() {
+ return mEndTimeNano - mStartTimeNano;
+ }
+
+ public float getDurationSeconds() {
+ return (float) getDurationNanos() / NANOS_TO_SECONDS;
+ }
+
+ public ArrayList<Point> getPoints() {
+ return mPoints;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
new file mode 100644
index 000000000..8abd7e2ec
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 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.incallui.answer.impl.classifier;
+
+/** An abstract class for classifiers which classify each stroke separately. */
+abstract class StrokeClassifier extends Classifier {
+
+ /**
+ * @param stroke the stroke for which the evaluation will be calculated
+ * @return a non-negative value which is used to determine whether this a false touch; the bigger
+ * the value the greater the chance that this a false touch
+ */
+ public abstract float getFalseTouchEvaluation(Stroke stroke);
+}