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
diff --git a/packages/Keyguard/Android.mk b/packages/Keyguard/Android.mk
index 1f2b5fb..5b08674 100644
--- a/packages/Keyguard/Android.mk
+++ b/packages/Keyguard/Android.mk
@@ -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_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))
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
index 40087cd..8738288 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java
@@ -21,11 +21,11 @@
 
 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 @@
     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 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 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 @@
             }
             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 @@
                 Slog.w(TAG, "Exception calling onShown():", e);
             }
         }
+        if (mAnalyticsCallback != null) {
+            mAnalyticsCallback.onScreenOn();
+        }
     }
 
     public synchronized void verifyUnlock() {
@@ -487,6 +504,10 @@
     public synchronized void hide() {
         if (DEBUG) Log.d(TAG, "hide()");
 
+        if (mAnalyticsCallback != null) {
+            mAnalyticsCallback.onHide();
+        }
+
         if (mKeyguardHost != null) {
             mKeyguardHost.setVisibility(View.GONE);
 
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
index 151177e..31e806c 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java
@@ -19,6 +19,7 @@
 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.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 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 @@
 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 @@
      */
     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 @@
 
     private KeyguardViewManager mKeyguardViewManager;
 
+    private final KeyguardAnalytics mKeyguardAnalytics;
+
     // these are protected by synchronized (this)
 
     /**
@@ -528,12 +542,25 @@
                 && !mLockPatternUtils.isLockScreenDisabled();
 
         WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
-
-        mKeyguardViewManager = new KeyguardViewManager(context, wm, mViewMediatorCallback,
-                mLockPatternUtils);
-
         final ContentResolver cr = mContext.getContentResolver();
 
+        if (ENABLE_ANALYTICS && !LockPatternUtils.isSafeModeEnabled() &&
+                Settings.Secure.getInt(cr, KEYGUARD_ANALYTICS_SETTING, 0) == 1) {
+            mKeyguardAnalytics = new KeyguardAnalytics(context, new SessionTypeAdapter() {
+
+                @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();
 
         mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0);
@@ -631,6 +658,9 @@
             } else {
                 doKeyguardLocked(null);
             }
+            if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) {
+                mKeyguardAnalytics.getCallback().onScreenOff();
+            }
         }
         KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurndOff(why);
     }
@@ -869,6 +899,9 @@
                 updateActivityLockScreenState();
                 adjustStatusBarLocked();
             }
+            if (ENABLE_ANALYTICS && mKeyguardAnalytics != null) {
+                mKeyguardAnalytics.getCallback().onSetHidden(isHidden);
+            }
         }
     }
 
diff --git a/packages/Keyguard/src/com/android/keyguard/analytics/KeyguardAnalytics.java b/packages/Keyguard/src/com/android/keyguard/analytics/KeyguardAnalytics.java
new file mode 100644
index 0000000..48ad16b
--- /dev/null
+++ b/packages/Keyguard/src/com/android/keyguard/analytics/KeyguardAnalytics.java
@@ -0,0 +1,280 @@
+/*
+ * 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();
+                    }
+                }
+            }
+        }
+    };
+
+}
diff --git a/packages/Keyguard/src/com/android/keyguard/analytics/PointerTracker.java b/packages/Keyguard/src/com/android/keyguard/analytics/PointerTracker.java
new file mode 100644
index 0000000..e68f751
--- /dev/null
+++ b/packages/Keyguard/src/com/android/keyguard/analytics/PointerTracker.java
@@ -0,0 +1,109 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/packages/Keyguard/src/com/android/keyguard/analytics/Session.java b/packages/Keyguard/src/com/android/keyguard/analytics/Session.java
new file mode 100644
index 0000000..05f9165
--- /dev/null
+++ b/packages/Keyguard/src/com/android/keyguard/analytics/Session.java
@@ -0,0 +1,220 @@
+/*
+ * 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;
+    }
+}
diff --git a/packages/Keyguard/src/com/android/keyguard/analytics/keyguard_analytics.proto b/packages/Keyguard/src/com/android/keyguard/analytics/keyguard_analytics.proto
new file mode 100644
index 0000000..68b1590
--- /dev/null
+++ b/packages/Keyguard/src/com/android/keyguard/analytics/keyguard_analytics.proto
@@ -0,0 +1,102 @@
+/*
+ * 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;
+}