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