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.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 for each point from a stroke, it creates a point on plane with coordinates
27  * (timeOffsetNano, distanceCoveredUpToThisPoint) (scaled by DURATION_SCALE and LENGTH_SCALE) and
28  * then it calculates the angle variance of these points like the class {@link AnglesClassifier}
29  * (without splitting it into two parts). The classifier ignores the last point of a stroke because
30  * the UP event comes in with some delay and this ruins the smoothness of this curve. Additionally,
31  * the classifier classifies calculates the percentage of angles which value is in [PI -
32  * ANGLE_DEVIATION, 2* PI) interval. The reason why the classifier does that is because the speed of
33  * a good stroke is most often increases, so most of these angels should be in this interval.
34  */
35 class SpeedAnglesClassifier extends StrokeClassifier {
36   private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
37 
SpeedAnglesClassifier(ClassifierData classifierData)38   public SpeedAnglesClassifier(ClassifierData classifierData) {
39     mClassifierData = classifierData;
40   }
41 
42   @Override
getTag()43   public String getTag() {
44     return "SPD_ANG";
45   }
46 
47   @Override
onTouchEvent(MotionEvent event)48   public void onTouchEvent(MotionEvent event) {
49     int action = event.getActionMasked();
50 
51     if (action == MotionEvent.ACTION_DOWN) {
52       mStrokeMap.clear();
53     }
54 
55     for (int i = 0; i < event.getPointerCount(); i++) {
56       Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
57 
58       if (mStrokeMap.get(stroke) == null) {
59         mStrokeMap.put(stroke, new Data());
60       }
61 
62       if (action != MotionEvent.ACTION_UP
63           && action != MotionEvent.ACTION_CANCEL
64           && !(action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
65         mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
66       }
67     }
68   }
69 
70   @Override
getFalseTouchEvaluation(Stroke stroke)71   public float getFalseTouchEvaluation(Stroke stroke) {
72     Data data = mStrokeMap.get(stroke);
73     return SpeedVarianceEvaluator.evaluate(data.getAnglesVariance())
74         + SpeedAnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
75   }
76 
77   private static class Data {
78     private static final float DURATION_SCALE = 1e8f;
79     private static final float LENGTH_SCALE = 1.0f;
80     private static final float ANGLE_DEVIATION = (float) Math.PI / 10.0f;
81 
82     private List<Point> mLastThreePoints = new ArrayList<>();
83     private Point mPreviousPoint;
84     private float mPreviousAngle;
85     private float mSumSquares;
86     private float mSum;
87     private float mCount;
88     private float mDist;
89     private float mAnglesCount;
90     private float mAcceleratingAngles;
91 
Data()92     public Data() {
93       mPreviousPoint = null;
94       mPreviousAngle = (float) Math.PI;
95       mSumSquares = 0.0f;
96       mSum = 0.0f;
97       mCount = 1.0f;
98       mDist = 0.0f;
99       mAnglesCount = mAcceleratingAngles = 0.0f;
100     }
101 
addPoint(Point point)102     public void addPoint(Point point) {
103       if (mPreviousPoint != null) {
104         mDist += mPreviousPoint.dist(point);
105       }
106 
107       mPreviousPoint = point;
108       Point speedPoint =
109           new Point((float) point.timeOffsetNano / DURATION_SCALE, mDist / LENGTH_SCALE);
110 
111       // Checking if the added point is different than the previously added point
112       // Repetitions are being ignored so that proper angles are calculated.
113       if (mLastThreePoints.isEmpty()
114           || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(speedPoint)) {
115         mLastThreePoints.add(speedPoint);
116         if (mLastThreePoints.size() == 4) {
117           mLastThreePoints.remove(0);
118 
119           float angle =
120               mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
121 
122           mAnglesCount++;
123           if (angle >= (float) Math.PI - ANGLE_DEVIATION) {
124             mAcceleratingAngles++;
125           }
126 
127           float difference = angle - mPreviousAngle;
128           mSum += difference;
129           mSumSquares += difference * difference;
130           mCount += 1.0f;
131           mPreviousAngle = angle;
132         }
133       }
134     }
135 
getAnglesVariance()136     public float getAnglesVariance() {
137       return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
138     }
139 
getAnglesPercentage()140     public float getAnglesPercentage() {
141       if (mAnglesCount == 0.0f) {
142         return 1.0f;
143       }
144       return (mAcceleratingAngles) / mAnglesCount;
145     }
146   }
147 }
148