Home | History | Annotate | Download | only in classifier
      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