Merge "Add HistoryEvaluator"

This commit is contained in:
Blazej Magnowski
2015-09-24 01:28:51 +00:00
committed by Android (Google) Code Review
10 changed files with 271 additions and 70 deletions

View File

@@ -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 @@ import java.util.List;
/**
* 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 @@ public class AnglesVarianceClassifier extends Classifier {
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 class AnglesVarianceClassifier extends Classifier {
}
public float getAnglesVariance() {
return mSumSquares / mCount + (mSum / mCount) * (mSum / mCount);
return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
}
}
}

View File

@@ -20,7 +20,7 @@ import android.hardware.SensorEvent;
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 abstract class Classifier {
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 abstract class Classifier {
*/
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);
}

View File

@@ -19,21 +19,26 @@ package com.android.systemui.classifier;
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 @@ public class ClassifierData {
}
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);
@@ -55,6 +66,13 @@ public class ClassifierData {
}
}
/**
* @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

View File

@@ -126,11 +126,10 @@ public class FalsingManager implements SensorEventListener {
}
/**
* @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 class FalsingManager implements SensorEventListener {
}
public void onQsDown() {
mHumanInteractionClassifier.setType(Classifier.QUICK_SETTINGS);
mDataCollector.onQsDown();
}
@@ -197,6 +197,7 @@ public class FalsingManager implements SensorEventListener {
}
public void onTrackingStarted() {
mHumanInteractionClassifier.setType(Classifier.UNLOCK);
mDataCollector.onTrackingStarted();
}
@@ -217,6 +218,7 @@ public class FalsingManager implements SensorEventListener {
}
public void onNotificatonStartDraggingDown() {
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DRAG_DOWN);
mDataCollector.onNotificatonStartDraggingDown();
}
@@ -229,6 +231,7 @@ public class FalsingManager implements SensorEventListener {
}
public void onNotificatonStartDismissing() {
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
mDataCollector.onNotificatonStartDismissing();
}
@@ -245,6 +248,11 @@ public class FalsingManager implements SensorEventListener {
}
public void onAffordanceSwipingStarted(boolean rightCorner) {
if (rightCorner) {
mHumanInteractionClassifier.setType(Classifier.RIGHT_AFFORDANCE);
} else {
mHumanInteractionClassifier.setType(Classifier.LEFT_AFFORDANCE);
}
mDataCollector.onAffordanceSwipingStarted(rightCorner);
}

View File

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

View File

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

View File

@@ -25,6 +25,8 @@ import android.os.UserHandle;
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 @@ public class HumanInteractionClassifier extends Classifier {
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 @@ public class HumanInteractionClassifier extends Classifier {
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 @@ public class HumanInteractionClassifier extends Classifier {
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 class HumanInteractionClassifier extends Classifier {
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() {

View File

@@ -19,7 +19,8 @@ package com.android.systemui.classifier;
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<>();

View File

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

View File

@@ -610,7 +610,7 @@ public abstract class PanelView extends FrameLayout {
if (!mStatusBar.isFalsingThresholdNeeded()) {
return false;
}
if (mFalsingManager.isFalseTouch(Classifier.UNLOCK)) {
if (mFalsingManager.isFalseTouch()) {
return true;
}
if (!mTouchAboveFalsingThreshold) {