Add HistoryEvaluator

Adds the possibility to store the evaluations for previous strokes and gestures.
Also and enables to take the history into account when classifying current
interatcions.

Change-Id: Ia8fa54a00daa80b4e5aebf11b11b568ed23165d4
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/AnglesVarianceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/AnglesVarianceClassifier.java
index 5cd914f..8c681fc 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/AnglesVarianceClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/AnglesVarianceClassifier.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.classifier;
 
-import android.hardware.SensorEvent;
 import android.view.MotionEvent;
 
 import java.lang.Math;
@@ -27,27 +26,19 @@
 
 /**
  * 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. The return value of the classifier is the variance of the differences
- * from a stroke. If there are multiple strokes created at once, the classifier sums up the
- * variances of all the strokes. Also the value is multiplied by HISTORY_FACTOR after each
- * INTERVAL milliseconds.
+ * 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. The return value of the classifier is 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.
  */
-public class AnglesVarianceClassifier extends Classifier {
-    private final float INTERVAL = 10.0f;
-    private final float CLEAR_HISTORY = 500f;
-    private final float HISTORY_FACTOR = 0.9f;
-
+public class AnglesVarianceClassifier extends StrokeClassifier {
     private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
-    private float mValue;
-    private long mLastUpdate;
 
     public AnglesVarianceClassifier(ClassifierData classifierData) {
         mClassifierData = classifierData;
-        mValue = 0.0f;
-        mLastUpdate = System.currentTimeMillis();
     }
 
     @Override
@@ -65,40 +56,12 @@
                 mStrokeMap.put(stroke, new Data());
             }
             mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
-
-            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
-                    || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
-                decayValue();
-                mValue += mStrokeMap.get(stroke).getAnglesVariance();
-            }
         }
     }
 
-    /**
-     * Decreases mValue through time
-     */
-    private void decayValue() {
-        long currentTimeMillis = System.currentTimeMillis();
-        if (currentTimeMillis - mLastUpdate > CLEAR_HISTORY) {
-            mValue = 0.0f;
-        } else {
-            mValue *= Math.pow(HISTORY_FACTOR, (float) (currentTimeMillis - mLastUpdate) / INTERVAL);
-        }
-        mLastUpdate = currentTimeMillis;
-    }
-
     @Override
-    public void onSensorChanged(SensorEvent event) {
-    }
-
-    @Override
-    public float getFalseTouchEvaluation(int type) {
-        decayValue();
-        float currentValue = 0.0f;
-        for (Data data: mStrokeMap.values()) {
-            currentValue += data.getAnglesVariance();
-        }
-        return (float) (mValue + currentValue);
+    public float getFalseTouchEvaluation(int type, Stroke stroke) {
+        return mStrokeMap.get(stroke).getAnglesVariance();
     }
 
     private class Data {
@@ -150,7 +113,7 @@
         }
 
         public float getAnglesVariance() {
-            return mSumSquares / mCount + (mSum / mCount) * (mSum / mCount);
+            return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
         }
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index b76be14..89d20de 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -20,7 +20,7 @@
 import android.view.MotionEvent;
 
 /**
- * An interface for classifiers for touch and sensor events.
+ * An abstract class for classifiers for touch and sensor events.
  */
 public abstract class Classifier {
     public static final int QUICK_SETTINGS = 0;
@@ -30,6 +30,7 @@
     public static final int UNLOCK = 4;
     public static final int LEFT_AFFORDANCE = 5;
     public static final int RIGHT_AFFORDANCE = 6;
+    public static final int GENERIC = 7;
 
     /**
      * Contains all the information about touch events from which the classifier can query
@@ -47,11 +48,4 @@
      */
     public void onSensorChanged(SensorEvent event) {
     }
-
-    /**
-     * @param type the type of action for which this method is called
-     * @return a nonnegative value which is used to determine whether this a false touch. The
-     *         bigger the value the greater the chance that this a false touch.
-     */
-    public abstract float getFalseTouchEvaluation(int type);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ClassifierData.java b/packages/SystemUI/src/com/android/systemui/classifier/ClassifierData.java
index 77b81d2..bccad4e 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ClassifierData.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ClassifierData.java
@@ -19,21 +19,26 @@
 import android.util.SparseArray;
 import android.view.MotionEvent;
 
+import java.util.ArrayList;
+
 /**
  * Contains data which is used to classify interaction sequences on the lockscreen. It does, for
  * example, provide information on the current touch state.
  */
 public class ClassifierData {
     private SparseArray<Stroke> mCurrentStrokes = new SparseArray<>();
+    private ArrayList<Stroke> mEndingStrokes = new ArrayList<>();
 
     public ClassifierData() {
     }
 
     public void update(MotionEvent event) {
+        mEndingStrokes.clear();
         int action = event.getActionMasked();
         if (action == MotionEvent.ACTION_DOWN) {
             mCurrentStrokes.clear();
         }
+
         for (int i = 0; i < event.getPointerCount(); i++) {
             int id = event.getPointerId(i);
             if (mCurrentStrokes.get(id) == null) {
@@ -41,10 +46,16 @@
             }
             mCurrentStrokes.get(id).addPoint(event.getX(i), event.getY(i),
                     event.getEventTimeNano());
+
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
+                    || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+                mEndingStrokes.add(getStroke(id));
+            }
         }
     }
 
     public void cleanUp(MotionEvent event) {
+        mEndingStrokes.clear();
         int action = event.getActionMasked();
         for (int i = 0; i < event.getPointerCount(); i++) {
             int id = event.getPointerId(i);
@@ -56,6 +67,13 @@
     }
 
     /**
+     * @return the list of Strokes which are ending in the recently added MotionEvent
+     */
+    public ArrayList<Stroke> getEndingStrokes() {
+        return mEndingStrokes;
+    }
+
+    /**
      * @param id the id from MotionEvent
      * @return the Stroke assigned to the id
      */
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
index 347273a..c68fff8 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManager.java
@@ -126,11 +126,10 @@
     }
 
     /**
-     * @param type the type of action for which this method is called
      * @return true if the classifier determined that this is not a human interacting with the phone
      */
-    public boolean isFalseTouch(int type) {
-        return mHumanInteractionClassifier.getFalseTouchEvaluation(type) > 0.5;
+    public boolean isFalseTouch() {
+        return mHumanInteractionClassifier.isFalseTouch();
     }
 
     @Override
@@ -189,6 +188,7 @@
     }
 
     public void onQsDown() {
+        mHumanInteractionClassifier.setType(Classifier.QUICK_SETTINGS);
         mDataCollector.onQsDown();
     }
 
@@ -197,6 +197,7 @@
     }
 
     public void onTrackingStarted() {
+        mHumanInteractionClassifier.setType(Classifier.UNLOCK);
         mDataCollector.onTrackingStarted();
     }
 
@@ -217,6 +218,7 @@
     }
 
     public void onNotificatonStartDraggingDown() {
+        mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DRAG_DOWN);
         mDataCollector.onNotificatonStartDraggingDown();
     }
 
@@ -229,6 +231,7 @@
     }
 
     public void onNotificatonStartDismissing() {
+        mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
         mDataCollector.onNotificatonStartDismissing();
     }
 
@@ -245,6 +248,11 @@
     }
 
     public void onAffordanceSwipingStarted(boolean rightCorner) {
+        if (rightCorner) {
+            mHumanInteractionClassifier.setType(Classifier.RIGHT_AFFORDANCE);
+        } else {
+            mHumanInteractionClassifier.setType(Classifier.LEFT_AFFORDANCE);
+        }
         mDataCollector.onAffordanceSwipingStarted(rightCorner);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/GestureClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/GestureClassifier.java
new file mode 100644
index 0000000..e7f4c35
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/GestureClassifier.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2015 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.systemui.classifier;
+
+/**
+ * An abstract class for classifiers which classify the whole gesture (all the strokes which
+ * occurred from DOWN event to UP/CANCEL event)
+ */
+public abstract class GestureClassifier extends Classifier {
+
+    /**
+     * @param type the type of action for which this method is called
+     * @return a non-negative value which is used to determine whether the most recent gesture is a
+     *         false interaction. The bigger the value the greater the chance that this a false
+     *         interaction.
+     */
+    public abstract float getFalseTouchEvaluation(int type);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/HistoryEvaluator.java b/packages/SystemUI/src/com/android/systemui/classifier/HistoryEvaluator.java
new file mode 100644
index 0000000..b057bda
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/HistoryEvaluator.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 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.systemui.classifier;
+
+import java.util.ArrayList;
+
+/**
+ * Holds the evaluations for ended strokes and gestures. These values are decreased through time.
+ */
+public class HistoryEvaluator {
+    private static final float INTERVAL = 50.0f;
+    private static final float HISTORY_FACTOR = 0.9f;
+    private static final float EPSILON = 1e-5f;
+
+    private final ArrayList<Data> mStrokes = new ArrayList<>();
+    private final ArrayList<Data> mGestureWeights = new ArrayList<>();
+    private long mLastUpdate;
+
+    public HistoryEvaluator() {
+        mLastUpdate = System.currentTimeMillis();
+    }
+
+    public void addStroke(float evaluation) {
+        decayValue();
+        mStrokes.add(new Data(evaluation));
+    }
+
+    public void addGesture(float evaluation) {
+        decayValue();
+        mGestureWeights.add(new Data(evaluation));
+    }
+
+    /**
+     * Calculates the weighted average of strokes and adds to it the weighted average of gestures
+     */
+    public float getEvaluation() {
+        return weightedAverage(mStrokes) + weightedAverage(mGestureWeights);
+    }
+
+    private float weightedAverage(ArrayList<Data> list) {
+        float sumValue = 0.0f;
+        float sumWeight = 0.0f;
+        int size = list.size();
+        for (int i = 0; i < size; i++) {
+            Data data = list.get(i);
+            sumValue += data.evaluation * data.weight;
+            sumWeight += data.weight;
+        }
+
+        if (sumWeight == 0.0f) {
+            return 0.0f;
+        }
+
+        return sumValue / sumWeight;
+    }
+
+    private void decayValue() {
+        long currentTimeMillis = System.currentTimeMillis();
+
+        // All weights are multiplied by HISTORY_FACTOR after each INTERVAL milliseconds.
+        float factor = (float) Math.pow(HISTORY_FACTOR,
+                (float) (currentTimeMillis - mLastUpdate) / INTERVAL);
+
+        decayValue(mStrokes, factor);
+        decayValue(mGestureWeights, factor);
+        mLastUpdate = currentTimeMillis;
+    }
+
+    private void decayValue(ArrayList<Data> list, float factor) {
+        int size = list.size();
+        for (int i = 0; i < size; i++) {
+            list.get(i).weight *= factor;
+        }
+
+        // Removing evaluations with such small weights that they do not matter anymore
+        while (!list.isEmpty() && isZero(list.get(0).weight)) {
+            list.remove(0);
+        }
+    }
+
+    private boolean isZero(float x) {
+        return x <= EPSILON && x >= -EPSILON;
+    }
+
+    /**
+     * For each stroke it holds its initial value and the current weight. Initially the
+     * weight is set to 1.0
+     */
+    private class Data {
+        public float evaluation;
+        public float weight;
+
+        public Data(float evaluation) {
+            this.evaluation = evaluation;
+            weight = 1.0f;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
index a5f6df85..86ea640 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/HumanInteractionClassifier.java
@@ -25,6 +25,8 @@
 import android.provider.Settings;
 import android.view.MotionEvent;
 
+import java.util.ArrayList;
+
 /**
  * An classifier trying to determine whether it is a human interacting with the phone or not.
  */
@@ -35,8 +37,14 @@
     private final Handler mHandler = new Handler();
     private final Context mContext;
 
-    private AnglesVarianceClassifier mAnglesVarianceClassifier;
+    private ArrayList<StrokeClassifier> mStrokeClassifiers = new ArrayList<>();
+    private ArrayList<GestureClassifier> mGestureClassifiers = new ArrayList<>();
+    private final int mStrokeClassifiersSize;
+    private final int mGestureClassifiersSize;
+
+    private HistoryEvaluator mHistoryEvaluator;
     private boolean mEnableClassifier = false;
+    private int mCurrentType = Classifier.GENERIC;
 
     protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
         @Override
@@ -48,7 +56,12 @@
     private HumanInteractionClassifier(Context context) {
         mContext = context;
         mClassifierData = new ClassifierData();
-        mAnglesVarianceClassifier = new AnglesVarianceClassifier(mClassifierData);
+        mHistoryEvaluator = new HistoryEvaluator();
+
+        mStrokeClassifiers.add(new AnglesVarianceClassifier(mClassifierData));
+
+        mStrokeClassifiersSize = mStrokeClassifiers.size();
+        mGestureClassifiersSize = mGestureClassifiers.size();
 
         mContext.getContentResolver().registerContentObserver(
                 Settings.Global.getUriFor(HIC_ENABLE), false,
@@ -71,11 +84,44 @@
                 HIC_ENABLE, 0);
     }
 
+    public void setType(int type) {
+        mCurrentType = type;
+    }
+
     @Override
     public void onTouchEvent(MotionEvent event) {
         if (mEnableClassifier) {
             mClassifierData.update(event);
-            mAnglesVarianceClassifier.onTouchEvent(event);
+
+            for (int i = 0; i < mStrokeClassifiersSize; i++) {
+                mStrokeClassifiers.get(i).onTouchEvent(event);
+            }
+
+            for (int i = 0; i < mGestureClassifiersSize; i++) {
+                mGestureClassifiers.get(i).onTouchEvent(event);
+            }
+
+            int size = mClassifierData.getEndingStrokes().size();
+            for (int i = 0; i < size; i++) {
+                Stroke stroke = mClassifierData.getEndingStrokes().get(i);
+                float evaluation = 0.0f;
+                for (int j = 0; j < mStrokeClassifiersSize; j++) {
+                    evaluation += mStrokeClassifiers.get(j).getFalseTouchEvaluation(
+                            mCurrentType, stroke);
+                }
+                mHistoryEvaluator.addStroke(evaluation);
+            }
+
+            int action = event.getActionMasked();
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+                float evaluation = 0.0f;
+                for (int i = 0; i < mGestureClassifiersSize; i++) {
+                    evaluation += mGestureClassifiers.get(i).getFalseTouchEvaluation(mCurrentType);
+                }
+                mHistoryEvaluator.addGesture(evaluation);
+                setType(Classifier.GENERIC);
+            }
+
             mClassifierData.cleanUp(event);
         }
     }
@@ -84,12 +130,8 @@
     public void onSensorChanged(SensorEvent event) {
     }
 
-    @Override
-    public float getFalseTouchEvaluation(int type) {
-        if (mEnableClassifier) {
-            return mAnglesVarianceClassifier.getFalseTouchEvaluation(type);
-        }
-        return 0.0f;
+    public boolean isFalseTouch() {
+        return mHistoryEvaluator.getEvaluation() >= 5.0f;
     }
 
     public boolean isEnabled() {
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Stroke.java b/packages/SystemUI/src/com/android/systemui/classifier/Stroke.java
index f386cbe4..8c3fdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Stroke.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Stroke.java
@@ -19,7 +19,8 @@
 import java.util.ArrayList;
 
 /**
- * Contains data about movement traces (pointers)
+ * Contains data about a stroke (a single trace, all the events from a given id from the
+ * DOWN/POINTER_DOWN event till the UP/POINTER_UP/CANCEL event.)
  */
 public class Stroke {
     private ArrayList<Point> mPoints = new ArrayList<>();
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/StrokeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/StrokeClassifier.java
new file mode 100644
index 0000000..d561f46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/classifier/StrokeClassifier.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 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.systemui.classifier;
+
+/**
+ * An abstract class for classifiers which classify each stroke separately.
+ */
+public abstract class StrokeClassifier extends Classifier {
+
+    /**
+     * @param type the type of action for which this method is called
+     * @param stroke the stroke for which the evaluation will be calculated
+     * @return a non-negative value which is used to determine whether this a false touch. The
+     *         bigger the value the greater the chance that this a false touch.
+     */
+    public abstract float getFalseTouchEvaluation(int type, Stroke stroke);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 13d0e1e..3feead8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -610,7 +610,7 @@
         if (!mStatusBar.isFalsingThresholdNeeded()) {
             return false;
         }
-        if (mFalsingManager.isFalseTouch(Classifier.UNLOCK)) {
+        if (mFalsingManager.isFalseTouch()) {
             return true;
         }
         if (!mTouchAboveFalsingThreshold) {