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) {