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

Commit 58b58fe6 authored by Adrian Roos's avatar Adrian Roos
Browse files

Add session analytics to Keyguard.

On eng and userdebug builds, adds the possibility to track touch and sensor events on the Keyguard.

Change-Id: I9ff9fe5545cb9b7e6833a6af0b5a97a6c204dbd2
parent 1cdd7dda
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -16,7 +16,8 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

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

LOCAL_PACKAGE_NAME := Keyguard

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

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

LOCAL_PROTOC_OPTIMIZE_TYPE := nano
LOCAL_PROTO_JAVA_OUTPUT_PARAMS := optional_field_style=accessors

include $(BUILD_PACKAGE)

#include $(call all-makefiles-under,$(LOCAL_PATH))
+23 −2
Original line number Diff line number Diff line
@@ -21,11 +21,11 @@ import android.graphics.drawable.BitmapDrawable;

import com.android.internal.policy.IKeyguardShowCallback;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.analytics.KeyguardAnalytics;

import org.xmlpull.v1.XmlPullParser;

import android.app.ActivityManager;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -76,6 +76,7 @@ public class KeyguardViewManager {
    private final Context mContext;
    private final ViewManager mViewManager;
    private final KeyguardViewMediator.ViewMediatorCallback mViewMediatorCallback;
    private final KeyguardAnalytics.Callback mAnalyticsCallback;

    private WindowManager.LayoutParams mWindowLayoutParams;
    private boolean mNeedsInput = false;
@@ -107,11 +108,12 @@ public class KeyguardViewManager {
     */
    public KeyguardViewManager(Context context, ViewManager viewManager,
            KeyguardViewMediator.ViewMediatorCallback callback,
            LockPatternUtils lockPatternUtils) {
            LockPatternUtils lockPatternUtils, KeyguardAnalytics.Callback analyticsCallback) {
        mContext = context;
        mViewManager = viewManager;
        mViewMediatorCallback = callback;
        mLockPatternUtils = lockPatternUtils;
        mAnalyticsCallback = analyticsCallback;
    }

    /**
@@ -120,6 +122,9 @@ public class KeyguardViewManager {
     */
    public synchronized void show(Bundle options) {
        if (DEBUG) Log.d(TAG, "show(); mKeyguardView==" + mKeyguardView);
        if (mAnalyticsCallback != null) {
            mAnalyticsCallback.onShow();
        }

        boolean enableScreenRotation = shouldEnableScreenRotation();

@@ -262,6 +267,15 @@ public class KeyguardViewManager {
            }
            return super.dispatchKeyEvent(event);
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            boolean result = false;
            if (mAnalyticsCallback != null) {
                result = mAnalyticsCallback.onTouchEvent(ev, getWidth(), getHeight()) || result;
            }
            return super.dispatchTouchEvent(ev) || result;
        }
    }

    SparseArray<Parcelable> mStateContainer = new SparseArray<Parcelable>();
@@ -473,6 +487,9 @@ public class KeyguardViewManager {
                Slog.w(TAG, "Exception calling onShown():", e);
            }
        }
        if (mAnalyticsCallback != null) {
            mAnalyticsCallback.onScreenOn();
        }
    }

    public synchronized void verifyUnlock() {
@@ -487,6 +504,10 @@ public class KeyguardViewManager {
    public synchronized void hide() {
        if (DEBUG) Log.d(TAG, "hide()");

        if (mAnalyticsCallback != null) {
            mAnalyticsCallback.onHide();
        }

        if (mKeyguardHost != null) {
            mKeyguardHost.setVisibility(View.GONE);

+36 −3
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.keyguard;
import com.android.internal.policy.IKeyguardExitCallback;
import com.android.internal.policy.IKeyguardShowCallback;
import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT;
import static com.android.keyguard.analytics.KeyguardAnalytics.SessionTypeAdapter;

import android.app.Activity;
import android.app.ActivityManagerNative;
@@ -33,6 +34,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -54,6 +56,10 @@ import android.view.WindowManagerPolicy;

import com.android.internal.telephony.IccCardConstants;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.analytics.Session;
import com.android.keyguard.analytics.KeyguardAnalytics;

import java.io.File;


/**
@@ -100,6 +106,7 @@ import com.android.internal.widget.LockPatternUtils;
public class KeyguardViewMediator {
    private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000;
    final static boolean DEBUG = false;
    private static final boolean ENABLE_ANALYTICS = Build.IS_DEBUGGABLE;
    private final static boolean DBG_WAKE = false;

    private final static String TAG = "KeyguardViewMediator";
@@ -161,6 +168,11 @@ public class KeyguardViewMediator {
     */
    private static final boolean ALLOW_NOTIFICATIONS_DEFAULT = false;

    /**
     * Secure setting whether analytics are collected on the keyguard.
     */
    private static final String KEYGUARD_ANALYTICS_SETTING = "keyguard_analytics";

    /** The stream type that the lock sounds are tied to. */
    private int mMasterStreamType;

@@ -194,6 +206,8 @@ public class KeyguardViewMediator {

    private KeyguardViewManager mKeyguardViewManager;

    private final KeyguardAnalytics mKeyguardAnalytics;

    // these are protected by synchronized (this)

    /**
@@ -528,11 +542,24 @@ public class KeyguardViewMediator {
                && !mLockPatternUtils.isLockScreenDisabled();

        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        final ContentResolver cr = mContext.getContentResolver();

        mKeyguardViewManager = new KeyguardViewManager(context, wm, mViewMediatorCallback,
                mLockPatternUtils);
        if (ENABLE_ANALYTICS && !LockPatternUtils.isSafeModeEnabled() &&
                Settings.Secure.getInt(cr, KEYGUARD_ANALYTICS_SETTING, 0) == 1) {
            mKeyguardAnalytics = new KeyguardAnalytics(context, new SessionTypeAdapter() {

        final ContentResolver cr = mContext.getContentResolver();
                @Override
                public int getSessionType() {
                    return mLockPatternUtils.isSecure() ? Session.TYPE_KEYGUARD_SECURE
                            : Session.TYPE_KEYGUARD_INSECURE;
                }
            }, new File(mContext.getCacheDir(), "keyguard_analytics.bin"));
        } else {
            mKeyguardAnalytics = null;
        }
        mKeyguardViewManager = new KeyguardViewManager(context, wm, mViewMediatorCallback,
                mLockPatternUtils,
                mKeyguardAnalytics != null ? mKeyguardAnalytics.getCallback() : null);

        mScreenOn = mPM.isScreenOn();

@@ -631,6 +658,9 @@ public class KeyguardViewMediator {
            } else {
                doKeyguardLocked(null);
            }
            if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) {
                mKeyguardAnalytics.getCallback().onScreenOff();
            }
        }
        KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurndOff(why);
    }
@@ -869,6 +899,9 @@ public class KeyguardViewMediator {
                updateActivityLockScreenState();
                adjustStatusBarLocked();
            }
            if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) {
                mKeyguardAnalytics.getCallback().onSetHidden(isHidden);
            }
        }
    }

+280 −0
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 String ANALYTICS_FILE = "/sdcard/keyguard_analytics.bin";

    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 onSetHidden(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 onSetHidden(boolean hidden) {
            synchronized (KeyguardAnalytics.this) {
                if (hidden != mHidden) {
                    if (DEBUG) {
                        Log.d(TAG, "onSetHidden(" + 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();
                    }
                }
            }
        }
    };

}
+109 −0
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);
        }
    }
}
Loading