1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.incallui.answer.impl.classifier; 18 19 import android.util.ArrayMap; 20 import android.view.MotionEvent; 21 import java.util.ArrayList; 22 import java.util.List; 23 import java.util.Map; 24 25 /** 26 * A classifier which calculates the variance of differences between successive angles in a stroke. 27 * For each stroke it keeps its last three points. If some successive points are the same, it 28 * ignores the repetitions. If a new point is added, the classifier calculates the angle between the 29 * last three points. After that, it calculates the difference between this angle and the previously 30 * calculated angle. Then it calculates the variance of the differences from a stroke. To the 31 * differences there is artificially added value 0.0 and the difference between the first angle and 32 * PI (angles are in radians). It helps with strokes which have few points and punishes more strokes 33 * which are not smooth. 34 * 35 * <p>This classifier also tries to split the stroke into two parts in the place in which the 36 * biggest angle is. It calculates the angle variance of the two parts and sums them up. The reason 37 * the classifier is doing this, is because some human swipes at the beginning go for a moment in 38 * one direction and then they rapidly change direction for the rest of the stroke (like a tick). 39 * The final result is the minimum of angle variance of the whole stroke and the sum of angle 40 * variances of the two parts split up. The classifier tries the tick option only if the first part 41 * is shorter than the second part. 42 * 43 * <p>Additionally, the classifier classifies the angles as left angles (those angles which value is 44 * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles ([PI - ANGLE_DEVIATION, PI + 45 * ANGLE_DEVIATION] interval) and right angles ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then 46 * calculates the percentage of angles which are in the same direction (straight angles can be left 47 * angels or right angles) 48 */ 49 class AnglesClassifier extends StrokeClassifier { 50 private Map<Stroke, Data> mStrokeMap = new ArrayMap<>(); 51 52 public AnglesClassifier(ClassifierData classifierData) { 53 mClassifierData = classifierData; 54 } 55 56 @Override 57 public String getTag() { 58 return "ANG"; 59 } 60 61 @Override 62 public void onTouchEvent(MotionEvent event) { 63 int action = event.getActionMasked(); 64 65 if (action == MotionEvent.ACTION_DOWN) { 66 mStrokeMap.clear(); 67 } 68 69 for (int i = 0; i < event.getPointerCount(); i++) { 70 Stroke stroke = mClassifierData.getStroke(event.getPointerId(i)); 71 72 if (mStrokeMap.get(stroke) == null) { 73 mStrokeMap.put(stroke, new Data()); 74 } 75 mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1)); 76 } 77 } 78 79 @Override 80 public float getFalseTouchEvaluation(Stroke stroke) { 81 Data data = mStrokeMap.get(stroke); 82 return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance()) 83 + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage()); 84 } 85 86 private static class Data { 87 private static final float ANGLE_DEVIATION = (float) Math.PI / 20.0f; 88 private static final float MIN_MOVE_DIST_DP = .01f; 89 90 private List<Point> mLastThreePoints = new ArrayList<>(); 91 private float mFirstAngleVariance; 92 private float mPreviousAngle; 93 private float mBiggestAngle; 94 private float mSumSquares; 95 private float mSecondSumSquares; 96 private float mSum; 97 private float mSecondSum; 98 private float mCount; 99 private float mSecondCount; 100 private float mFirstLength; 101 private float mLength; 102 private float mAnglesCount; 103 private float mLeftAngles; 104 private float mRightAngles; 105 private float mStraightAngles; 106 107 public Data() { 108 mFirstAngleVariance = 0.0f; 109 mPreviousAngle = (float) Math.PI; 110 mBiggestAngle = 0.0f; 111 mSumSquares = mSecondSumSquares = 0.0f; 112 mSum = mSecondSum = 0.0f; 113 mCount = mSecondCount = 1.0f; 114 mLength = mFirstLength = 0.0f; 115 mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f; 116 } 117 118 public void addPoint(Point point) { 119 // Checking if the added point is different than the previously added point 120 // Repetitions and short distances are being ignored so that proper angles are calculated. 121 if (mLastThreePoints.isEmpty() 122 || (!mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point) 123 && (mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point) 124 > MIN_MOVE_DIST_DP))) { 125 if (!mLastThreePoints.isEmpty()) { 126 mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point); 127 } 128 mLastThreePoints.add(point); 129 if (mLastThreePoints.size() == 4) { 130 mLastThreePoints.remove(0); 131 132 float angle = 133 mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2)); 134 135 mAnglesCount++; 136 if (angle < Math.PI - ANGLE_DEVIATION) { 137 mLeftAngles++; 138 } else if (angle <= Math.PI + ANGLE_DEVIATION) { 139 mStraightAngles++; 140 } else { 141 mRightAngles++; 142 } 143 144 float difference = angle - mPreviousAngle; 145 146 // If this is the biggest angle of the stroke so then we save the value of 147 // the angle variance so far and start to count the values for the angle 148 // variance of the second part. 149 if (mBiggestAngle < angle) { 150 mBiggestAngle = angle; 151 mFirstLength = mLength; 152 mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount); 153 mSecondSumSquares = 0.0f; 154 mSecondSum = 0.0f; 155 mSecondCount = 1.0f; 156 } else { 157 mSecondSum += difference; 158 mSecondSumSquares += difference * difference; 159 mSecondCount += 1.0f; 160 } 161 162 mSum += difference; 163 mSumSquares += difference * difference; 164 mCount += 1.0f; 165 mPreviousAngle = angle; 166 } 167 } 168 } 169 170 public float getAnglesVariance(float sumSquares, float sum, float count) { 171 return sumSquares / count - (sum / count) * (sum / count); 172 } 173 174 public float getAnglesVariance() { 175 float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount); 176 if (mFirstLength < mLength / 2f) { 177 anglesVariance = 178 Math.min( 179 anglesVariance, 180 mFirstAngleVariance 181 + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount)); 182 } 183 return anglesVariance; 184 } 185 186 public float getAnglesPercentage() { 187 if (mAnglesCount == 0.0f) { 188 return 1.0f; 189 } 190 return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount; 191 } 192 } 193 } 194