/* * 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. * *

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. * *

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 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 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; } } }