Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0e2ffbd4 authored by Blazej Magnowski's avatar Blazej Magnowski
Browse files

Add FalsingManager and Classifier to SystemUI

Adds the possibility to analyze and classify touch and sensor events as
human or false touches.

Change-Id: I5079c02406d532fea38ca2d302e8606effae0696
parent b16e7469
Loading
Loading
Loading
Loading
+39 −84
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
@@ -30,8 +29,6 @@ import android.provider.Settings;
import android.util.Log;
import android.view.MotionEvent;

import com.android.systemui.statusbar.StatusBarState;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -47,94 +44,70 @@ import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.P
 * A session starts when the screen is turned on.
 * A session ends when the screen is turned off or user unlocks the phone.
 */
public class LockedPhoneAnalytics implements SensorEventListener {
    private static final String TAG = "LockedPhoneAnalytics";
    private static final String ANALYTICS_ENABLE = "locked_phone_analytics_enable";
    private static final String ENFORCE_BOUNCER = "locked_phone_analytics_enforce_bouncer";
    private static final String COLLECT_BAD_TOCUHES = "locked_phone_analytics_collect_bad_touches";
public class DataCollector implements SensorEventListener {
    private static final String TAG = "DataCollector";
    private static final String COLLECTOR_ENABLE = "data_collector_enable";
    private static final String COLLECT_BAD_TOUCHES = "data_collector_collect_bad_touches";

    private static final long TIMEOUT_MILLIS = 11000; // 11 seconds.
    public static final boolean DEBUG = false;

    private static final int[] SENSORS = new int[] {
            Sensor.TYPE_ACCELEROMETER,
            Sensor.TYPE_GYROSCOPE,
            Sensor.TYPE_PROXIMITY,
            Sensor.TYPE_LIGHT,
            Sensor.TYPE_ROTATION_VECTOR,
    };

    private final Handler mHandler = new Handler();
    protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
        @Override
        public void onChange(boolean selfChange) {
            updateConfiguration();
        }
    };

    private final SensorManager mSensorManager;
    private final Context mContext;

    // Err on the side of caution, so logging is not started after a crash even tough the screen
    // is off.
    private SensorLoggerSession mCurrentSession = null;

    private boolean mEnableAnalytics = false;
    private boolean mEnforceBouncer = false;
    private boolean mEnableCollector = false;
    private boolean mTimeoutActive = false;
    private boolean mCollectBadTouches = false;
    private boolean mBouncerOn = false;
    private boolean mCornerSwiping = false;
    private boolean mTrackingStarted = false;

    private int mState = StatusBarState.SHADE;
    private static DataCollector sInstance = null;

    private static LockedPhoneAnalytics sInstance = null;
    protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
        @Override
        public void onChange(boolean selfChange) {
            updateConfiguration();
        }
    };

    private LockedPhoneAnalytics(Context context) {
        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
    private DataCollector(Context context) {
        mContext = context;

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(ANALYTICS_ENABLE), false,
                Settings.Secure.getUriFor(COLLECTOR_ENABLE), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(ENFORCE_BOUNCER), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        mContext.getContentResolver().registerContentObserver(
                Settings.Secure.getUriFor(COLLECT_BAD_TOCUHES), false,
                Settings.Secure.getUriFor(COLLECT_BAD_TOUCHES), false,
                mSettingsObserver,
                UserHandle.USER_ALL);

        updateConfiguration();
    }

    public static LockedPhoneAnalytics getInstance(Context context) {
    public static DataCollector getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new LockedPhoneAnalytics(context);
            sInstance = new DataCollector(context);
        }
        return sInstance;
    }

    private void updateConfiguration() {
        mEnableAnalytics = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                ANALYTICS_ENABLE, 0);
        mEnforceBouncer = mEnableAnalytics && 0 != Settings.Secure.getInt(
        mEnableCollector = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                ENFORCE_BOUNCER, 0);
        mCollectBadTouches = mEnableAnalytics && 0 != Settings.Secure.getInt(
                COLLECTOR_ENABLE, 0);
        mCollectBadTouches = mEnableCollector && 0 != Settings.Secure.getInt(
                mContext.getContentResolver(),
                COLLECT_BAD_TOCUHES, 0);
                COLLECT_BAD_TOUCHES, 0);
    }

    private boolean sessionEntrypoint() {
        if ((mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)
                && mEnableAnalytics && mCurrentSession == null) {
        if (mEnableCollector && mCurrentSession == null) {
            onSessionStart();
            return true;
        }
@@ -142,22 +115,15 @@ public class LockedPhoneAnalytics implements SensorEventListener {
    }

    private void sessionExitpoint(int result) {
        if (mEnableAnalytics && mCurrentSession != null) {
        if (mEnableCollector && mCurrentSession != null) {
            onSessionEnd(result);
        }
    }

    private void onSessionStart() {
        mBouncerOn = false;
        mCornerSwiping = false;
        mTrackingStarted = false;
        mCurrentSession = new SensorLoggerSession(System.currentTimeMillis(), System.nanoTime());
        for (int sensorType : SENSORS) {
            Sensor s = mSensorManager.getDefaultSensor(sensorType);
            if (s != null) {
                mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
            }
        }
    }

    private void onSessionEnd(int result) {
@@ -196,10 +162,9 @@ public class LockedPhoneAnalytics implements SensorEventListener {
        });
    }


    @Override
    public synchronized void onSensorChanged(SensorEvent event) {
        if (mEnableAnalytics && mCurrentSession != null) {
        if (mEnableCollector && mCurrentSession != null) {
            mCurrentSession.addSensorEvent(event, System.nanoTime());
            enforceTimeout();
        }
@@ -221,18 +186,14 @@ public class LockedPhoneAnalytics implements SensorEventListener {
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    public boolean shouldEnforceBouncer() {
        return mEnforceBouncer;
    public boolean isEnabled() {
        return mEnableCollector;
    }

    public void setStatusBarState(int state) {
        mState = state;
    }

    public void onScreenOn() {
    public void onScreenTurningOn() {
        if (sessionEntrypoint()) {
            if (DEBUG) {
                Log.d(TAG, "onScreenOn");
                Log.d(TAG, "onScreenTurningOn");
            }
            addEvent(PhoneEvent.ON_SCREEN_ON);
        }
@@ -264,24 +225,18 @@ public class LockedPhoneAnalytics implements SensorEventListener {
    }

    public void onBouncerShown() {
        if (!mBouncerOn) {
        if (DEBUG) {
            Log.d(TAG, "onBouncerShown");
        }
            mBouncerOn = true;
        addEvent(PhoneEvent.ON_BOUNCER_SHOWN);
    }
    }

    public void onBouncerHidden() {
        if (mBouncerOn) {
        if (DEBUG) {
            Log.d(TAG, "onBouncerHidden");
        }
            mBouncerOn = false;
        addEvent(PhoneEvent.ON_BOUNCER_HIDDEN);
    }
    }

    public void onQsDown() {
        if (DEBUG) {
@@ -433,20 +388,20 @@ public class LockedPhoneAnalytics implements SensorEventListener {
        addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_HINT_STARTED);
    }

    public void onTouchEvent(MotionEvent ev, int width, int height) {
        if (!mBouncerOn && mCurrentSession != null) {
    public void onTouchEvent(MotionEvent event, int width, int height) {
        if (mCurrentSession != null) {
            if (DEBUG) {
                Log.v(TAG, "onTouchEvent(ev.action="
                        + MotionEvent.actionToString(ev.getAction()) + ")");
                        + MotionEvent.actionToString(event.getAction()) + ")");
            }
            mCurrentSession.addMotionEvent(ev);
            mCurrentSession.addMotionEvent(event);
            mCurrentSession.setTouchArea(width, height);
            enforceTimeout();
        }
    }

    private void addEvent(int eventType) {
        if (mEnableAnalytics && mCurrentSession != null) {
        if (mEnableCollector && mCurrentSession != null) {
            mCurrentSession.addPhoneEvent(eventType, System.nanoTime());
        }
    }
+1 −1
Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ public class SensorLoggerSession {
        mResult = result;
        mEndTimestampMillis = endTimestampMillis;

        if (LockedPhoneAnalytics.DEBUG) {
        if (DataCollector.DEBUG) {
            Log.d(TAG, "Ending session result=" + result + " it lasted for " +
                    (float) (mEndTimestampMillis - mStartTimestampMillis) / 1000f + "s");
        }
+156 −0
Original line number Diff line number Diff line
/*
 * 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 android.hardware.SensorEvent;
import android.view.MotionEvent;

import java.lang.Math;

import java.util.ArrayList;
import java.util.HashMap;
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.
 */
public class AnglesVarianceClassifier extends Classifier {
    private final float INTERVAL = 10.0f;
    private final float CLEAR_HISTORY = 500f;
    private final float HISTORY_FACTOR = 0.9f;

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

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

    private class Data {
        private List<Point> mLastThreePoints = new ArrayList<>();
        private float mPreviousAngle;
        private float mSumSquares;
        private float mSum;
        private float mCount;

        public Data() {
            mPreviousAngle = (float) Math.PI;
            mSumSquares = 0.0f;
            mSum = 0.0f;
            mCount = 1.0f;
        }

        public void addPoint(Point point) {
            // Checking if the added point is different than the previously added point
            // Repetitions are being ignored so that proper angles are calculated.
            if (mLastThreePoints.isEmpty()
                    || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)) {
                mLastThreePoints.add(point);
                if (mLastThreePoints.size() == 4) {
                    mLastThreePoints.remove(0);

                    float angle = getAngle(mLastThreePoints.get(0), mLastThreePoints.get(1),
                            mLastThreePoints.get(2));

                    float difference = angle - mPreviousAngle;
                    mSum += difference;
                    mSumSquares += difference * difference;
                    mCount += 1.0;
                    mPreviousAngle = angle;
                }
            }
        }

        private float getAngle(Point a, Point b, Point c) {
            float dist1 = a.dist(b);
            float dist2 = b.dist(c);
            float crossProduct = b.crossProduct(a, c);
            float dotProduct = b.dotProduct(a, c);
            float cos = Math.min(1.0f, Math.max(-1.0f, dotProduct / dist1 / dist2));
            float angle = (float) Math.acos(cos);
            if (crossProduct < 0.0) {
                angle = 2.0f * (float) Math.PI - angle;
            }
            return angle;
        }

        public float getAnglesVariance() {
            return mSumSquares / mCount + (mSum / mCount) * (mSum / mCount);
        }
    }
}
 No newline at end of file
+57 −0
Original line number Diff line number Diff line
/*
 * 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 android.hardware.SensorEvent;
import android.view.MotionEvent;

/**
 * An interface for classifiers for touch and sensor events.
 */
public abstract class Classifier {
    public static final int QUICK_SETTINGS = 0;
    public static final int NOTIFICATION_DISMISS = 1;
    public static final int NOTIFICATION_DRAG_DOWN = 2;
    public static final int NOTIFICATION_DOUBLE_TAP = 3;
    public static final int UNLOCK = 4;
    public static final int LEFT_AFFORDANCE = 5;
    public static final int RIGHT_AFFORDANCE = 6;

    /**
     * Contains all the information about touch events from which the classifier can query
     */
    protected ClassifierData mClassifierData;

    /**
     * Informs the classifier that a new touch event has occurred
     */
    public void onTouchEvent(MotionEvent event) {
    }

    /**
     * Informs the classifier that a sensor change occurred
     */
    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);
}
+65 −0
Original line number Diff line number Diff line
/*
 * 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 android.util.SparseArray;
import android.view.MotionEvent;

/**
 * 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<>();

    public ClassifierData() {
    }

    public void update(MotionEvent event) {
        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) {
                mCurrentStrokes.put(id, new Stroke(event.getEventTimeNano()));
            }
            mCurrentStrokes.get(id).addPoint(event.getX(i), event.getY(i),
                    event.getEventTimeNano());
        }
    }

    public void cleanUp(MotionEvent event) {
        int action = event.getActionMasked();
        for (int i = 0; i < event.getPointerCount(); i++) {
            int id = event.getPointerId(i);
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
                    || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
                mCurrentStrokes.remove(id);
            }
        }
    }

    /**
     * @param id the id from MotionEvent
     * @return the Stroke assigned to the id
     */
    public Stroke getStroke(int id) {
        return mCurrentStrokes.get(id);
    }
}
Loading