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

Commit f8053993 authored by Adrian Roos's avatar Adrian Roos Committed by Android (Google) Code Review
Browse files

Merge "Remove now defunct touch analysis facility"

parents 71b197a0 bba9a212
Loading
Loading
Loading
Loading
+1 −5
Original line number Diff line number Diff line
@@ -16,8 +16,7 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-subdir-Iaidl-files) \
                   $(call all-proto-files-under,src)
LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-subdir-Iaidl-files)

LOCAL_MODULE := Keyguard

@@ -27,9 +26,6 @@ LOCAL_PRIVILEGED_MODULE := true

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

LOCAL_PROTOC_OPTIMIZE_TYPE := nano
LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors

LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res

include $(BUILD_STATIC_JAVA_LIBRARY)
+0 −278
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.keyguard.analytics;

import com.google.protobuf.nano.CodedOutputByteBufferNano;
import com.google.protobuf.nano.MessageNano;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.AsyncTask;
import android.util.Log;
import android.view.MotionEvent;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Tracks sessions, touch and sensor events in Keyguard.
 *
 * A session starts when the user is presented with the Keyguard and ends when the Keyguard is no
 * longer visible to the user.
 */
public class KeyguardAnalytics implements SensorEventListener {

    private static final boolean DEBUG = false;
    private static final String TAG = "KeyguardAnalytics";
    private static final long TIMEOUT_MILLIS = 11000; // 11 seconds.

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

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

    private final SensorManager mSensorManager;
    private final SessionTypeAdapter mSessionTypeAdapter;
    private final File mAnalyticsFile;

    public KeyguardAnalytics(Context context, SessionTypeAdapter sessionTypeAdapter,
            File analyticsFile) {
        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        mSessionTypeAdapter = sessionTypeAdapter;
        mAnalyticsFile = analyticsFile;
    }

    public Callback getCallback() {
        return mCallback;
    }

    public interface Callback {
        public void onShow();
        public void onHide();
        public void onScreenOn();
        public void onScreenOff();
        public boolean onTouchEvent(MotionEvent ev, int width, int height);
        public void onSetOccluded(boolean hidden);
    }

    public interface SessionTypeAdapter {
        public int getSessionType();
    }

    private void sessionEntrypoint() {
        if (mCurrentSession == null && mScreenOn && !mHidden) {
            onSessionStart();
        }
    }

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

    private void onSessionStart() {
        int type = mSessionTypeAdapter.getSessionType();
        mCurrentSession = new Session(System.currentTimeMillis(), System.nanoTime(), type);
        if (type == Session.TYPE_KEYGUARD_SECURE) {
            mCurrentSession.setRedactTouchEvents();
        }
        for (int sensorType : SENSORS) {
            Sensor s = mSensorManager.getDefaultSensor(sensorType);
            if (s != null) {
                mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
            }
        }
        if (DEBUG) {
            Log.d(TAG, "onSessionStart()");
        }
    }

    private void onSessionEnd(int result) {
        if (DEBUG) {
            Log.d(TAG, String.format("onSessionEnd(success=%d)", result));
        }
        mSensorManager.unregisterListener(this);

        Session session = mCurrentSession;
        mCurrentSession = null;

        session.end(System.currentTimeMillis(), result);
        queueSession(session);
    }

    private void queueSession(final Session currentSession) {
        if (DEBUG) {
            Log.i(TAG, "Saving session.");
        }
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                try {
                    byte[] b = writeDelimitedProto(currentSession.toProto());
                    OutputStream os = new FileOutputStream(mAnalyticsFile, true /* append */);
                    if (DEBUG) {
                        Log.d(TAG, String.format("Serialized size: %d kB.", b.length / 1024));
                    }
                    try {
                        os.write(b);
                        os.flush();
                    } finally {
                        try {
                            os.close();
                        } catch (IOException e) {
                            Log.e(TAG, "Exception while closing file", e);
                        }
                    }
                } catch (IOException e) {
                    Log.e(TAG, "Exception while writing file", e);
                }
                return null;
            }

            private byte[] writeDelimitedProto(MessageNano proto)
                    throws IOException {
                byte[] result = new byte[CodedOutputByteBufferNano.computeMessageSizeNoTag(proto)];
                CodedOutputByteBufferNano ob = CodedOutputByteBufferNano.newInstance(result);
                ob.writeMessageNoTag(proto);
                ob.checkNoSpaceLeft();
                return result;
            }
        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
    }

    @Override
    public synchronized void onSensorChanged(SensorEvent event) {
        if (false) {
            Log.v(TAG, String.format(
                    "onSensorChanged(name=%s, values[0]=%f)",
                    event.sensor.getName(), event.values[0]));
        }
        if (mCurrentSession != null) {
            mCurrentSession.addSensorEvent(event, System.nanoTime());
            enforceTimeout();
        }
    }

    private void enforceTimeout() {
        if (System.currentTimeMillis() - mCurrentSession.getStartTimestampMillis()
                > TIMEOUT_MILLIS) {
            onSessionEnd(Session.RESULT_UNKNOWN);
            if (DEBUG) {
                Log.i(TAG, "Analytics timed out.");
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    private final Callback mCallback = new Callback() {
        @Override
        public void onShow() {
            if (DEBUG) {
                Log.d(TAG, "onShow()");
            }
            synchronized (KeyguardAnalytics.this) {
                sessionEntrypoint();
            }
        }

        @Override
        public void onHide() {
            if (DEBUG) {
                Log.d(TAG, "onHide()");
            }
            synchronized (KeyguardAnalytics.this) {
                sessionExitpoint(Session.RESULT_SUCCESS);
            }
        }

        @Override
        public void onScreenOn() {
            if (DEBUG) {
                Log.d(TAG, "onScreenOn()");
            }
            synchronized (KeyguardAnalytics.this) {
                mScreenOn = true;
                sessionEntrypoint();
            }
        }

        @Override
        public void onScreenOff() {
            if (DEBUG) {
                Log.d(TAG, "onScreenOff()");
            }
            synchronized (KeyguardAnalytics.this) {
                mScreenOn = false;
                sessionExitpoint(Session.RESULT_FAILURE);
            }
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev, int width, int height) {
            if (DEBUG) {
                Log.v(TAG, "onTouchEvent(ev.action="
                        + MotionEvent.actionToString(ev.getAction()) + ")");
            }
            synchronized (KeyguardAnalytics.this) {
                if (mCurrentSession != null) {
                    mCurrentSession.addMotionEvent(ev);
                    mCurrentSession.setTouchArea(width, height);
                    enforceTimeout();
                }
            }
            return true;
        }

        @Override
        public void onSetOccluded(boolean hidden) {
            synchronized (KeyguardAnalytics.this) {
                if (hidden != mHidden) {
                    if (DEBUG) {
                        Log.d(TAG, "onSetOccluded(" + hidden + ")");
                    }
                    mHidden = hidden;
                    if (hidden) {
                        // Could have gone to camera on purpose / by falsing or an app could have
                        // launched on top of the lockscreen.
                        sessionExitpoint(Session.RESULT_UNKNOWN);
                    } else {
                        sessionEntrypoint();
                    }
                }
            }
        }
    };

}
+0 −109
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.keyguard.analytics;

import android.graphics.RectF;
import android.util.FloatMath;
import android.util.SparseArray;
import android.view.MotionEvent;

import java.util.HashMap;
import java.util.Map;

import static com.android.keyguard.analytics.KeyguardAnalyticsProtos.Session.TouchEvent.BoundingBox;

/**
 * Takes motion events and tracks the length and bounding box of each pointer gesture as well as
 * the bounding box of the whole gesture.
 */
public class PointerTracker {
    private SparseArray<Pointer> mPointerInfoMap = new SparseArray<Pointer>();
    private RectF mTotalBoundingBox = new RectF();

    public void addMotionEvent(MotionEvent ev) {
        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
            float x = ev.getX();
            float y = ev.getY();
            mTotalBoundingBox.set(x, y, x, y);
        }
        for (int i = 0; i < ev.getPointerCount(); i++) {
            int id = ev.getPointerId(i);
            Pointer pointer = getPointer(id);
            float x = ev.getX(i);
            float y = ev.getY(i);
            boolean down = ev.getActionMasked() == MotionEvent.ACTION_DOWN
                    || (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
                            && ev.getActionIndex() == i);
            pointer.addPoint(x, y, down);
            mTotalBoundingBox.union(x, y);
        }
    }

    public float getPointerLength(int id) {
        return getPointer(id).length;
    }

    public BoundingBox getBoundingBox() {
        return boundingBoxFromRect(mTotalBoundingBox);
    }

    public BoundingBox getPointerBoundingBox(int id) {
        return boundingBoxFromRect(getPointer(id).boundingBox);
    }

    private BoundingBox boundingBoxFromRect(RectF f) {
        BoundingBox bb = new BoundingBox();
        bb.setHeight(f.height());
        bb.setWidth(f.width());
        return bb;
    }

    private Pointer getPointer(int id) {
        Pointer p = mPointerInfoMap.get(id);
        if (p == null) {
            p = new Pointer();
            mPointerInfoMap.put(id, p);
        }
        return p;
    }

    private static class Pointer {
        public float length;
        public final RectF boundingBox = new RectF();

        private float mLastX;
        private float mLastY;

        public void addPoint(float x, float y, boolean down) {
            float deltaX;
            float deltaY;
            if (down) {
                boundingBox.set(x, y, x, y);
                length = 0f;
                deltaX = 0;
                deltaY = 0;
            } else {
                deltaX = x - mLastX;
                deltaY = y - mLastY;
            }
            mLastX = x;
            mLastY = y;
            length += FloatMath.sqrt(deltaX * deltaX + deltaY * deltaY);
            boundingBox.union(x, y);
        }
    }
}
+0 −220
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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.keyguard.analytics;

import android.os.Build;
import android.util.Slog;
import android.view.MotionEvent;

import java.util.ArrayList;

import static com.android.keyguard.analytics.KeyguardAnalyticsProtos.Session.SensorEvent;
import static com.android.keyguard.analytics.KeyguardAnalyticsProtos.Session.TouchEvent;

/**
 * Records data about one keyguard session.
 *
 * The recorded data contains start and end of the session, whether it unlocked the device
 * successfully, sensor data and touch data.
 *
 * If the keyguard is secure, the recorded touch data will correlate or contain the user pattern or
 * PIN. If this is not desired, the touch coordinates can be redacted before serialization.
 */
public class Session {

    private static final String TAG = "KeyguardAnalytics";
    private static final boolean DEBUG = false;

    /**
     * The user has failed to unlock the device in this session.
     */
    public static final int RESULT_FAILURE = KeyguardAnalyticsProtos.Session.FAILURE;
    /**
     * The user has succeeded in unlocking the device in this session.
     */
    public static final int RESULT_SUCCESS = KeyguardAnalyticsProtos.Session.SUCCESS;

    /**
     * It is unknown how the session with the keyguard ended.
     */
    public static final int RESULT_UNKNOWN = KeyguardAnalyticsProtos.Session.UNKNOWN;

    /**
     * This session took place on an insecure keyguard.
     */
    public static final int TYPE_KEYGUARD_INSECURE
            = KeyguardAnalyticsProtos.Session.KEYGUARD_INSECURE;

    /**
     * This session took place on an secure keyguard.
     */
    public static final int TYPE_KEYGUARD_SECURE
            = KeyguardAnalyticsProtos.Session.KEYGUARD_SECURE;

    /**
     * This session took place during a fake wake up of the device.
     */
    public static final int TYPE_RANDOM_WAKEUP = KeyguardAnalyticsProtos.Session.RANDOM_WAKEUP;


    private final PointerTracker mPointerTracker = new PointerTracker();

    private final long mStartTimestampMillis;
    private final long mStartSystemTimeNanos;
    private final int mType;

    private boolean mRedactTouchEvents;
    private ArrayList<TouchEvent> mMotionEvents = new ArrayList<TouchEvent>(200);
    private ArrayList<SensorEvent> mSensorEvents = new ArrayList<SensorEvent>(600);
    private int mTouchAreaHeight;
    private int mTouchAreaWidth;

    private long mEndTimestampMillis;
    private int mResult;
    private boolean mEnded;

    public Session(long startTimestampMillis, long startSystemTimeNanos, int type) {
        mStartTimestampMillis = startTimestampMillis;
        mStartSystemTimeNanos = startSystemTimeNanos;
        mType = type;
    }

    public void end(long endTimestampMillis, int result) {
        mEnded = true;
        mEndTimestampMillis = endTimestampMillis;
        mResult = result;
    }

    public void addMotionEvent(MotionEvent motionEvent) {
        if (mEnded) {
            return;
        }
        mPointerTracker.addMotionEvent(motionEvent);
        mMotionEvents.add(protoFromMotionEvent(motionEvent));
    }

    public void addSensorEvent(android.hardware.SensorEvent eventOrig, long systemTimeNanos) {
        if (mEnded) {
            return;
        }
        SensorEvent event = protoFromSensorEvent(eventOrig, systemTimeNanos);
        mSensorEvents.add(event);
        if (DEBUG) {
            Slog.v(TAG, String.format("addSensorEvent(name=%s, values[0]=%f",
                    event.getType(), event.values[0]));
        }
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Session{");
        sb.append("mType=").append(mType);
        sb.append(", mStartTimestampMillis=").append(mStartTimestampMillis);
        sb.append(", mStartSystemTimeNanos=").append(mStartSystemTimeNanos);
        sb.append(", mEndTimestampMillis=").append(mEndTimestampMillis);
        sb.append(", mResult=").append(mResult);
        sb.append(", mRedactTouchEvents=").append(mRedactTouchEvents);
        sb.append(", mTouchAreaHeight=").append(mTouchAreaHeight);
        sb.append(", mTouchAreaWidth=").append(mTouchAreaWidth);
        sb.append(", mMotionEvents=[size=").append(mMotionEvents.size()).append("]");
        sb.append(", mSensorEvents=[size=").append(mSensorEvents.size()).append("]");
        sb.append('}');
        return sb.toString();
    }

    public KeyguardAnalyticsProtos.Session toProto() {
        KeyguardAnalyticsProtos.Session proto = new KeyguardAnalyticsProtos.Session();
        proto.setStartTimestampMillis(mStartTimestampMillis);
        proto.setDurationMillis(mEndTimestampMillis - mStartTimestampMillis);
        proto.setBuild(Build.FINGERPRINT);
        proto.setResult(mResult);
        proto.sensorEvents = mSensorEvents.toArray(proto.sensorEvents);
        proto.touchEvents = mMotionEvents.toArray(proto.touchEvents);
        proto.setTouchAreaWidth(mTouchAreaWidth);
        proto.setTouchAreaHeight(mTouchAreaHeight);
        proto.setType(mType);
        if (mRedactTouchEvents) {
            redactTouchEvents(proto.touchEvents);
        }
        return proto;
    }

    private void redactTouchEvents(TouchEvent[] touchEvents) {
        for (int i = 0; i < touchEvents.length; i++) {
            TouchEvent t = touchEvents[i];
            for (int j = 0; j < t.pointers.length; j++) {
                TouchEvent.Pointer p = t.pointers[j];
                p.clearX();
                p.clearY();
            }
            t.setRedacted(true);
        }
    }

    private SensorEvent protoFromSensorEvent(android.hardware.SensorEvent ev, long sysTimeNanos) {
        SensorEvent proto = new SensorEvent();
        proto.setType(ev.sensor.getType());
        proto.setTimeOffsetNanos(sysTimeNanos - mStartSystemTimeNanos);
        proto.setTimestamp(ev.timestamp);
        proto.values = ev.values.clone();
        return proto;
    }

    private TouchEvent protoFromMotionEvent(MotionEvent ev) {
        int count = ev.getPointerCount();
        TouchEvent proto = new TouchEvent();
        proto.setTimeOffsetNanos(ev.getEventTimeNano() - mStartSystemTimeNanos);
        proto.setAction(ev.getActionMasked());
        proto.setActionIndex(ev.getActionIndex());
        proto.pointers = new TouchEvent.Pointer[count];
        for (int i = 0; i < count; i++) {
            TouchEvent.Pointer p = new TouchEvent.Pointer();
            p.setX(ev.getX(i));
            p.setY(ev.getY(i));
            p.setSize(ev.getSize(i));
            p.setPressure(ev.getPressure(i));
            p.setId(ev.getPointerId(i));
            proto.pointers[i] = p;
            if ((ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP && ev.getActionIndex() == i)
                    || ev.getActionMasked() == MotionEvent.ACTION_UP) {
                p.boundingBox = mPointerTracker.getPointerBoundingBox(p.getId());
                p.setLength(mPointerTracker.getPointerLength(p.getId()));
            }
        }
        if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
            proto.boundingBox = mPointerTracker.getBoundingBox();
        }
        return proto;
    }

    /**
     * Discards the x / y coordinates of the touch events on serialization. Retained are the
     * size of the individual and overall bounding boxes and the length of each pointer's gesture.
     */
    public void setRedactTouchEvents() {
        mRedactTouchEvents = true;
    }

    public void setTouchArea(int width, int height) {
        mTouchAreaWidth = width;
        mTouchAreaHeight = height;
    }

    public long getStartTimestampMillis() {
        return mStartTimestampMillis;
    }
}
+0 −102
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 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
 */

syntax = "proto2";

package keyguard;

option java_package = "com.android.keyguard.analytics";
option java_outer_classname = "KeyguardAnalyticsProtos";

message Session {
    message TouchEvent {
        message BoundingBox {
            optional float width = 1;
            optional float height = 2;
        }

        enum Action {
            // Keep in sync with MotionEvent.
            DOWN = 0;
            UP = 1;
            MOVE = 2;
            CANCEL = 3;
            OUTSIDE = 4;
            POINTER_DOWN = 5;
            POINTER_UP = 6;
        }

        message Pointer {
            optional float x = 1;
            optional float y = 2;
            optional float size = 3;
            optional float pressure = 4;
            optional int32 id = 5;
            optional float length = 6;
            // Bounding box of the pointer. Only set on UP or POINTER_UP event of this pointer.
            optional BoundingBox boundingBox = 7;
        }

        optional uint64 timeOffsetNanos = 1;
        optional Action action = 2;
        optional int32 actionIndex = 3;
        repeated Pointer pointers = 4;
        /* If true, the the x / y coordinates of the touch events were redacted. Retained are the
           size of the individual and overall bounding boxes and the length of each pointer's
           gesture. */
        optional bool redacted = 5;
        // Bounding box of the whole gesture. Only set on UP event.
        optional BoundingBox boundingBox = 6;
    }

    message SensorEvent {
        enum Type {
            ACCELEROMETER = 1;
            GYROSCOPE = 4;
            LIGHT = 5;
            PROXIMITY = 8;
            ROTATION_VECTOR = 11;
        }

        optional Type type = 1;
        optional uint64 timeOffsetNanos = 2;
        repeated float values = 3;
        optional uint64 timestamp = 4;
    }

    enum Result {
        FAILURE = 0;
        SUCCESS = 1;
        UNKNOWN = 2;
    }

    enum Type {
        KEYGUARD_INSECURE = 0;
        KEYGUARD_SECURE = 1;
        RANDOM_WAKEUP = 2;
    }

    optional uint64 startTimestampMillis = 1;
    optional uint64 durationMillis = 2;
    optional string build = 3;
    optional Result result = 4;
    repeated TouchEvent touchEvents = 5;
    repeated SensorEvent sensorEvents = 6;

    optional int32 touchAreaWidth = 9;
    optional int32 touchAreaHeight = 10;
    optional Type type = 11;
}
Loading