diff options
Diffstat (limited to 'java/com/android/incallui/answer/impl/classifier')
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); +} |