1 /*
2  * Copyright (C) 2015 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.systemui.classifier;
18 
19 import android.os.Build;
20 import android.os.SystemProperties;
21 import android.util.Log;
22 import android.view.MotionEvent;
23 
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.List;
27 
28 /**
29  * A classifier which calculates the variance of differences between successive angles in a stroke.
30  * For each stroke it keeps its last three points. If some successive points are the same, it
31  * ignores the repetitions. If a new point is added, the classifier calculates the angle between
32  * the last three points. After that, it calculates the difference between this angle and the
33  * previously calculated angle. Then it calculates the variance of the differences from a stroke.
34  * To the differences there is artificially added value 0.0 and the difference between the first
35  * angle and PI (angles are in radians). It helps with strokes which have few points and punishes
36  * more strokes which are not smooth.
37  *
38  * This classifier also tries to split the stroke into two parts in the place in which the biggest
39  * angle is. It calculates the angle variance of the two parts and sums them up. The reason the
40  * classifier is doing this, is because some human swipes at the beginning go for a moment in one
41  * direction and then they rapidly change direction for the rest of the stroke (like a tick). The
42  * final result is the minimum of angle variance of the whole stroke and the sum of angle variances
43  * of the two parts split up. The classifier tries the tick option only if the first part is
44  * shorter than the second part.
45  *
46  * Additionally, the classifier classifies the angles as left angles (those angles which value is
47  * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles
48  * ([PI - ANGLE_DEVIATION, PI + ANGLE_DEVIATION] interval) and right angles
49  * ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then calculates the percentage of angles which are
50  * in the same direction (straight angles can be left angels or right angles)
51  */
52 public class AnglesClassifier extends StrokeClassifier {
53     private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
54 
55     public static final boolean VERBOSE = SystemProperties.getBoolean("debug.falsing_log.ang",
56             Build.IS_DEBUGGABLE);
57 
58     private static String TAG = "ANG";
59 
AnglesClassifier(ClassifierData classifierData)60     public AnglesClassifier(ClassifierData classifierData) {
61         mClassifierData = classifierData;
62     }
63 
64     @Override
getTag()65     public String getTag() {
66         return TAG;
67     }
68 
69     @Override
onTouchEvent(MotionEvent event)70     public void onTouchEvent(MotionEvent event) {
71         int action = event.getActionMasked();
72 
73         if (action == MotionEvent.ACTION_DOWN) {
74             mStrokeMap.clear();
75         }
76 
77         for (int i = 0; i < event.getPointerCount(); i++) {
78             Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
79 
80             if (mStrokeMap.get(stroke) == null) {
81                 mStrokeMap.put(stroke, new Data());
82             }
83             mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
84         }
85     }
86 
87     @Override
getFalseTouchEvaluation(int type, Stroke stroke)88     public float getFalseTouchEvaluation(int type, Stroke stroke) {
89         Data data = mStrokeMap.get(stroke);
90         return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance(), type)
91                 + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage(), type);
92     }
93 
94     private static class Data {
95         private final float ANGLE_DEVIATION = (float) Math.PI / 20.0f;
96 
97         private List<Point> mLastThreePoints = new ArrayList<>();
98         private float mFirstAngleVariance;
99         private float mPreviousAngle;
100         private float mBiggestAngle;
101         private float mSumSquares;
102         private float mSecondSumSquares;
103         private float mSum;
104         private float mSecondSum;
105         private float mCount;
106         private float mSecondCount;
107         private float mFirstLength;
108         private float mLength;
109         private float mAnglesCount;
110         private float mLeftAngles;
111         private float mRightAngles;
112         private float mStraightAngles;
113 
Data()114         public Data() {
115             mFirstAngleVariance = 0.0f;
116             mPreviousAngle = (float) Math.PI;
117             mBiggestAngle = 0.0f;
118             mSumSquares = mSecondSumSquares = 0.0f;
119             mSum = mSecondSum = 0.0f;
120             mCount = mSecondCount = 1.0f;
121             mLength = mFirstLength = 0.0f;
122             mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f;
123         }
124 
addPoint(Point point)125         public void addPoint(Point point) {
126             // Checking if the added point is different than the previously added point
127             // Repetitions are being ignored so that proper angles are calculated.
128             if (mLastThreePoints.isEmpty()
129                     || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)) {
130                 if (!mLastThreePoints.isEmpty()) {
131                     mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point);
132                 }
133                 mLastThreePoints.add(point);
134                 if (mLastThreePoints.size() == 4) {
135                     mLastThreePoints.remove(0);
136 
137                     float angle = mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0),
138                             mLastThreePoints.get(2));
139 
140                     mAnglesCount++;
141                     if (angle < Math.PI - ANGLE_DEVIATION) {
142                         mLeftAngles++;
143                     } else if (angle <= Math.PI + ANGLE_DEVIATION) {
144                         mStraightAngles++;
145                     } else {
146                         mRightAngles++;
147                     }
148 
149                     float difference = angle - mPreviousAngle;
150 
151                     // If this is the biggest angle of the stroke so then we save the value of
152                     // the angle variance so far and start to count the values for the angle
153                     // variance of the second part.
154                     if (mBiggestAngle < angle) {
155                         mBiggestAngle = angle;
156                         mFirstLength = mLength;
157                         mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount);
158                         mSecondSumSquares = 0.0f;
159                         mSecondSum = 0.0f;
160                         mSecondCount = 1.0f;
161                     } else {
162                         mSecondSum += difference;
163                         mSecondSumSquares += difference * difference;
164                         mSecondCount += 1.0;
165                     }
166 
167                     mSum += difference;
168                     mSumSquares += difference * difference;
169                     mCount += 1.0;
170                     mPreviousAngle = angle;
171                 }
172             }
173         }
174 
getAnglesVariance(float sumSquares, float sum, float count)175         public float getAnglesVariance(float sumSquares, float sum, float count) {
176             return sumSquares / count - (sum / count) * (sum / count);
177         }
178 
getAnglesVariance()179         public float getAnglesVariance() {
180             float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
181             if (VERBOSE) {
182                 FalsingLog.i(TAG, "getAnglesVariance: (first pass) " + anglesVariance);
183                 FalsingLog.i(TAG, "   - mFirstLength=" + mFirstLength);
184                 FalsingLog.i(TAG, "   - mLength=" + mLength);
185             }
186             if (mFirstLength < mLength / 2f) {
187                 anglesVariance = Math.min(anglesVariance, mFirstAngleVariance
188                         + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
189                 if (VERBOSE) FalsingLog.i(TAG, "getAnglesVariance: (second pass) " + anglesVariance);
190             }
191             return anglesVariance;
192         }
193 
getAnglesPercentage()194         public float getAnglesPercentage() {
195             if (mAnglesCount == 0.0f) {
196                 if (VERBOSE) FalsingLog.i(TAG, "getAnglesPercentage: count==0, result=1");
197                 return 1.0f;
198             }
199             final float result = (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
200             if (VERBOSE) {
201                 FalsingLog.i(TAG, "getAnglesPercentage: left=" + mLeftAngles + " right="
202                         + mRightAngles + " straight=" + mStraightAngles + " count=" + mAnglesCount
203                         + " result=" + result);
204             }
205             return result;
206         }
207     }
208 }
209