2 * Copyright (C) 2015 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
17 package com.android.systemui.analytics;
19 import android.content.Context;
20 import android.database.ContentObserver;
21 import android.hardware.Sensor;
22 import android.hardware.SensorEvent;
23 import android.hardware.SensorEventListener;
24 import android.net.Uri;
25 import android.os.AsyncTask;
26 import android.os.Build;
27 import android.os.Handler;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.util.Log;
31 import android.view.MotionEvent;
32 import android.widget.Toast;
35 import java.io.FileOutputStream;
36 import java.io.IOException;
38 import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session;
39 import static com.android.systemui.statusbar.phone.TouchAnalyticsProto.Session.PhoneEvent;
42 * Tracks touch, sensor and phone events when the lockscreen is on. If the phone is unlocked
43 * the data containing these events is saved to a file. This data is collected
44 * to analyze how a human interaction looks like.
46 * A session starts when the screen is turned on.
47 * A session ends when the screen is turned off or user unlocks the phone.
49 public class DataCollector implements SensorEventListener {
50 private static final String TAG = "DataCollector";
51 private static final String COLLECTOR_ENABLE = "data_collector_enable";
52 private static final String COLLECT_BAD_TOUCHES = "data_collector_collect_bad_touches";
53 private static final String ALLOW_REJECTED_TOUCH_REPORTS =
54 "data_collector_allow_rejected_touch_reports";
56 private static final long TIMEOUT_MILLIS = 11000; // 11 seconds.
57 public static final boolean DEBUG = false;
59 private final Handler mHandler = new Handler();
60 private final Context mContext;
62 // Err on the side of caution, so logging is not started after a crash even tough the screen
64 private SensorLoggerSession mCurrentSession = null;
66 private boolean mEnableCollector = false;
67 private boolean mTimeoutActive = false;
68 private boolean mCollectBadTouches = false;
69 private boolean mCornerSwiping = false;
70 private boolean mTrackingStarted = false;
71 private boolean mAllowReportRejectedTouch = false;
73 private static DataCollector sInstance = null;
75 protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
77 public void onChange(boolean selfChange) {
78 updateConfiguration();
82 private DataCollector(Context context) {
85 mContext.getContentResolver().registerContentObserver(
86 Settings.Secure.getUriFor(COLLECTOR_ENABLE), false,
90 mContext.getContentResolver().registerContentObserver(
91 Settings.Secure.getUriFor(COLLECT_BAD_TOUCHES), false,
95 mContext.getContentResolver().registerContentObserver(
96 Settings.Secure.getUriFor(ALLOW_REJECTED_TOUCH_REPORTS), false,
100 updateConfiguration();
103 public static DataCollector getInstance(Context context) {
104 if (sInstance == null) {
105 sInstance = new DataCollector(context);
110 private void updateConfiguration() {
111 mEnableCollector = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
112 mContext.getContentResolver(),
113 COLLECTOR_ENABLE, 0);
114 mCollectBadTouches = mEnableCollector && 0 != Settings.Secure.getInt(
115 mContext.getContentResolver(),
116 COLLECT_BAD_TOUCHES, 0);
117 mAllowReportRejectedTouch = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
118 mContext.getContentResolver(),
119 ALLOW_REJECTED_TOUCH_REPORTS, 0);
122 private boolean sessionEntrypoint() {
123 if (isEnabled() && mCurrentSession == null) {
130 private void sessionExitpoint(int result) {
131 if (mCurrentSession != null) {
132 onSessionEnd(result);
136 private void onSessionStart() {
137 mCornerSwiping = false;
138 mTrackingStarted = false;
139 mCurrentSession = new SensorLoggerSession(System.currentTimeMillis(), System.nanoTime());
142 private void onSessionEnd(int result) {
143 SensorLoggerSession session = mCurrentSession;
144 mCurrentSession = null;
146 if (mEnableCollector) {
147 session.end(System.currentTimeMillis(), result);
148 queueSession(session);
152 public Uri reportRejectedTouch() {
153 if (mCurrentSession == null) {
154 Toast.makeText(mContext, "Generating rejected touch report failed: session timed out.",
155 Toast.LENGTH_LONG).show();
158 SensorLoggerSession currentSession = mCurrentSession;
160 currentSession.setType(Session.REJECTED_TOUCH_REPORT);
161 currentSession.end(System.currentTimeMillis(), Session.SUCCESS);
162 Session proto = currentSession.toProto();
164 byte[] b = Session.toByteArray(proto);
165 File dir = new File(mContext.getExternalCacheDir(), "rejected_touch_reports");
167 File touch = new File(dir, "rejected_touch_report_" + System.currentTimeMillis());
170 new FileOutputStream(touch).write(b);
171 } catch (IOException e) {
172 throw new RuntimeException(e);
175 return Uri.fromFile(touch);
178 private void queueSession(final SensorLoggerSession currentSession) {
179 AsyncTask.execute(new Runnable() {
182 byte[] b = Session.toByteArray(currentSession.toProto());
183 String dir = mContext.getFilesDir().getAbsolutePath();
184 if (currentSession.getResult() != Session.SUCCESS) {
185 if (!mCollectBadTouches) {
188 dir += "/bad_touches";
190 dir += "/good_touches";
193 File file = new File(dir);
195 File touch = new File(file, "trace_" + System.currentTimeMillis());
198 new FileOutputStream(touch).write(b);
199 } catch (IOException e) {
200 throw new RuntimeException(e);
207 public synchronized void onSensorChanged(SensorEvent event) {
208 if (isEnabled() && mCurrentSession != null) {
209 mCurrentSession.addSensorEvent(event, System.nanoTime());
214 private void enforceTimeout() {
215 if (mTimeoutActive) {
216 if (System.currentTimeMillis() - mCurrentSession.getStartTimestampMillis()
218 onSessionEnd(Session.UNKNOWN);
220 Log.i(TAG, "Analytics timed out.");
227 public void onAccuracyChanged(Sensor sensor, int accuracy) {
231 * @return true if data is being collected - either for data gathering or creating a
232 * rejected touch report.
234 public boolean isEnabled() {
235 return mEnableCollector || mAllowReportRejectedTouch;
239 * @return true if the full data set for data gathering should be collected - including
240 * extensive sensor data, which is is not normally included with rejected touch reports.
242 public boolean isEnabledFull() {
243 return mEnableCollector;
246 public void onScreenTurningOn() {
247 if (sessionEntrypoint()) {
249 Log.d(TAG, "onScreenTurningOn");
251 addEvent(PhoneEvent.ON_SCREEN_ON);
255 public void onScreenOnFromTouch() {
256 if (sessionEntrypoint()) {
258 Log.d(TAG, "onScreenOnFromTouch");
260 addEvent(PhoneEvent.ON_SCREEN_ON_FROM_TOUCH);
264 public void onScreenOff() {
266 Log.d(TAG, "onScreenOff");
268 addEvent(PhoneEvent.ON_SCREEN_OFF);
269 sessionExitpoint(Session.FAILURE);
272 public void onSucccessfulUnlock() {
274 Log.d(TAG, "onSuccessfulUnlock");
276 addEvent(PhoneEvent.ON_SUCCESSFUL_UNLOCK);
277 sessionExitpoint(Session.SUCCESS);
280 public void onBouncerShown() {
282 Log.d(TAG, "onBouncerShown");
284 addEvent(PhoneEvent.ON_BOUNCER_SHOWN);
287 public void onBouncerHidden() {
289 Log.d(TAG, "onBouncerHidden");
291 addEvent(PhoneEvent.ON_BOUNCER_HIDDEN);
294 public void onQsDown() {
296 Log.d(TAG, "onQsDown");
298 addEvent(PhoneEvent.ON_QS_DOWN);
301 public void setQsExpanded(boolean expanded) {
303 Log.d(TAG, "setQsExpanded = " + expanded);
306 addEvent(PhoneEvent.SET_QS_EXPANDED_TRUE);
308 addEvent(PhoneEvent.SET_QS_EXPANDED_FALSE);
312 public void onTrackingStarted() {
314 Log.d(TAG, "onTrackingStarted");
316 mTrackingStarted = true;
317 addEvent(PhoneEvent.ON_TRACKING_STARTED);
320 public void onTrackingStopped() {
321 if (mTrackingStarted) {
323 Log.d(TAG, "onTrackingStopped");
325 mTrackingStarted = false;
326 addEvent(PhoneEvent.ON_TRACKING_STOPPED);
330 public void onNotificationActive() {
332 Log.d(TAG, "onNotificationActive");
334 addEvent(PhoneEvent.ON_NOTIFICATION_ACTIVE);
338 public void onNotificationDoubleTap() {
340 Log.d(TAG, "onNotificationDoubleTap");
342 addEvent(PhoneEvent.ON_NOTIFICATION_DOUBLE_TAP);
345 public void setNotificationExpanded() {
347 Log.d(TAG, "setNotificationExpanded");
349 addEvent(PhoneEvent.SET_NOTIFICATION_EXPANDED);
352 public void onNotificatonStartDraggingDown() {
354 Log.d(TAG, "onNotificationStartDraggingDown");
356 addEvent(PhoneEvent.ON_NOTIFICATION_START_DRAGGING_DOWN);
359 public void onNotificatonStopDraggingDown() {
361 Log.d(TAG, "onNotificationStopDraggingDown");
363 addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DRAGGING_DOWN);
366 public void onNotificationDismissed() {
368 Log.d(TAG, "onNotificationDismissed");
370 addEvent(PhoneEvent.ON_NOTIFICATION_DISMISSED);
373 public void onNotificatonStartDismissing() {
375 Log.d(TAG, "onNotificationStartDismissing");
377 addEvent(PhoneEvent.ON_NOTIFICATION_START_DISMISSING);
380 public void onNotificatonStopDismissing() {
382 Log.d(TAG, "onNotificationStopDismissing");
384 addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DISMISSING);
387 public void onCameraOn() {
389 Log.d(TAG, "onCameraOn");
391 addEvent(PhoneEvent.ON_CAMERA_ON);
394 public void onLeftAffordanceOn() {
396 Log.d(TAG, "onLeftAffordanceOn");
398 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_ON);
401 public void onAffordanceSwipingStarted(boolean rightCorner) {
403 Log.d(TAG, "onAffordanceSwipingStarted");
405 mCornerSwiping = true;
407 addEvent(PhoneEvent.ON_RIGHT_AFFORDANCE_SWIPING_STARTED);
409 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_SWIPING_STARTED);
413 public void onAffordanceSwipingAborted() {
414 if (mCornerSwiping) {
416 Log.d(TAG, "onAffordanceSwipingAborted");
418 mCornerSwiping = false;
419 addEvent(PhoneEvent.ON_AFFORDANCE_SWIPING_ABORTED);
423 public void onUnlockHintStarted() {
425 Log.d(TAG, "onUnlockHintStarted");
427 addEvent(PhoneEvent.ON_UNLOCK_HINT_STARTED);
430 public void onCameraHintStarted() {
432 Log.d(TAG, "onCameraHintStarted");
434 addEvent(PhoneEvent.ON_CAMERA_HINT_STARTED);
437 public void onLeftAffordanceHintStarted() {
439 Log.d(TAG, "onLeftAffordanceHintStarted");
441 addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_HINT_STARTED);
444 public void onTouchEvent(MotionEvent event, int width, int height) {
445 if (mCurrentSession != null) {
447 Log.v(TAG, "onTouchEvent(ev.action="
448 + MotionEvent.actionToString(event.getAction()) + ")");
450 mCurrentSession.addMotionEvent(event);
451 mCurrentSession.setTouchArea(width, height);
456 private void addEvent(int eventType) {
457 if (isEnabled() && mCurrentSession != null) {
458 mCurrentSession.addPhoneEvent(eventType, System.nanoTime());
462 public boolean isReportingEnabled() {
463 return mAllowReportRejectedTouch;