1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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
15  */
16 
17 package com.android.systemui.analytics;
18 
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.Looper;
29 import android.os.UserHandle;
30 import android.provider.Settings;
31 import android.util.Log;
32 import android.view.MotionEvent;
33 import android.widget.Toast;
34 
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 
39 import static com.android.systemui.statusbar.phone.nano.TouchAnalyticsProto.Session;
40 import static com.android.systemui.statusbar.phone.nano.TouchAnalyticsProto.Session.PhoneEvent;
41 
42 /**
43  * Tracks touch, sensor and phone events when the lockscreen is on. If the phone is unlocked
44  * the data containing these events is saved to a file. This data is collected
45  * to analyze how a human interaction looks like.
46  *
47  * A session starts when the screen is turned on.
48  * A session ends when the screen is turned off or user unlocks the phone.
49  */
50 public class DataCollector implements SensorEventListener {
51     private static final String TAG = "DataCollector";
52     private static final String COLLECTOR_ENABLE = "data_collector_enable";
53     private static final String COLLECT_BAD_TOUCHES = "data_collector_collect_bad_touches";
54     private static final String ALLOW_REJECTED_TOUCH_REPORTS =
55             "data_collector_allow_rejected_touch_reports";
56 
57     private static final long TIMEOUT_MILLIS = 11000; // 11 seconds.
58     public static final boolean DEBUG = false;
59 
60     private final Handler mHandler = new Handler(Looper.getMainLooper());
61     private final Context mContext;
62 
63     // Err on the side of caution, so logging is not started after a crash even tough the screen
64     // is off.
65     private SensorLoggerSession mCurrentSession = null;
66 
67     private boolean mEnableCollector = false;
68     private boolean mTimeoutActive = false;
69     private boolean mCollectBadTouches = false;
70     private boolean mCornerSwiping = false;
71     private boolean mTrackingStarted = false;
72     private boolean mAllowReportRejectedTouch = false;
73 
74     private static DataCollector sInstance = null;
75 
76     protected final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
77         @Override
78         public void onChange(boolean selfChange) {
79             updateConfiguration();
80         }
81     };
82 
DataCollector(Context context)83     private DataCollector(Context context) {
84         mContext = context;
85 
86         mContext.getContentResolver().registerContentObserver(
87                 Settings.Secure.getUriFor(COLLECTOR_ENABLE), false,
88                 mSettingsObserver,
89                 UserHandle.USER_ALL);
90 
91         mContext.getContentResolver().registerContentObserver(
92                 Settings.Secure.getUriFor(COLLECT_BAD_TOUCHES), false,
93                 mSettingsObserver,
94                 UserHandle.USER_ALL);
95 
96         mContext.getContentResolver().registerContentObserver(
97                 Settings.Secure.getUriFor(ALLOW_REJECTED_TOUCH_REPORTS), false,
98                 mSettingsObserver,
99                 UserHandle.USER_ALL);
100 
101         updateConfiguration();
102     }
103 
getInstance(Context context)104     public static DataCollector getInstance(Context context) {
105         if (sInstance == null) {
106             sInstance = new DataCollector(context);
107         }
108         return sInstance;
109     }
110 
updateConfiguration()111     private void updateConfiguration() {
112         mEnableCollector = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
113                 mContext.getContentResolver(),
114                 COLLECTOR_ENABLE, 0);
115         mCollectBadTouches = mEnableCollector && 0 != Settings.Secure.getInt(
116                 mContext.getContentResolver(),
117                 COLLECT_BAD_TOUCHES, 0);
118         mAllowReportRejectedTouch = Build.IS_DEBUGGABLE && 0 != Settings.Secure.getInt(
119                 mContext.getContentResolver(),
120                 ALLOW_REJECTED_TOUCH_REPORTS, 0);
121     }
122 
sessionEntrypoint()123     private boolean sessionEntrypoint() {
124         if (isEnabled() && mCurrentSession == null) {
125             onSessionStart();
126             return true;
127         }
128         return false;
129     }
130 
sessionExitpoint(int result)131     private void sessionExitpoint(int result) {
132         if (mCurrentSession != null) {
133             onSessionEnd(result);
134         }
135     }
136 
onSessionStart()137     private void onSessionStart() {
138         mCornerSwiping = false;
139         mTrackingStarted = false;
140         mCurrentSession = new SensorLoggerSession(System.currentTimeMillis(), System.nanoTime());
141     }
142 
onSessionEnd(int result)143     private void onSessionEnd(int result) {
144         SensorLoggerSession session = mCurrentSession;
145         mCurrentSession = null;
146 
147         if (mEnableCollector) {
148             session.end(System.currentTimeMillis(), result);
149             queueSession(session);
150         }
151     }
152 
reportRejectedTouch()153     public Uri reportRejectedTouch() {
154         if (mCurrentSession == null) {
155             Toast.makeText(mContext, "Generating rejected touch report failed: session timed out.",
156                     Toast.LENGTH_LONG).show();
157             return null;
158         }
159         SensorLoggerSession currentSession = mCurrentSession;
160 
161         currentSession.setType(Session.REJECTED_TOUCH_REPORT);
162         currentSession.end(System.currentTimeMillis(), Session.SUCCESS);
163         Session proto = currentSession.toProto();
164 
165         byte[] b = Session.toByteArray(proto);
166         File dir = new File(mContext.getExternalCacheDir(), "rejected_touch_reports");
167         dir.mkdir();
168         File touch = new File(dir, "rejected_touch_report_" + System.currentTimeMillis());
169 
170         try {
171             new FileOutputStream(touch).write(b);
172         } catch (IOException e) {
173             throw new RuntimeException(e);
174         }
175 
176         return Uri.fromFile(touch);
177     }
178 
queueSession(final SensorLoggerSession currentSession)179     private void queueSession(final SensorLoggerSession currentSession) {
180         AsyncTask.execute(new Runnable() {
181             @Override
182             public void run() {
183                 byte[] b = Session.toByteArray(currentSession.toProto());
184                 String dir = mContext.getFilesDir().getAbsolutePath();
185                 if (currentSession.getResult() != Session.SUCCESS) {
186                     if (!mCollectBadTouches) {
187                         return;
188                     }
189                     dir += "/bad_touches";
190                 } else {
191                     dir += "/good_touches";
192                 }
193 
194                 File file = new File(dir);
195                 file.mkdir();
196                 File touch = new File(file, "trace_" + System.currentTimeMillis());
197 
198                 try {
199                     new FileOutputStream(touch).write(b);
200                 } catch (IOException e) {
201                     throw new RuntimeException(e);
202                 }
203             }
204         });
205     }
206 
207     @Override
onSensorChanged(SensorEvent event)208     public synchronized void onSensorChanged(SensorEvent event) {
209         if (isEnabled() && mCurrentSession != null) {
210             mCurrentSession.addSensorEvent(event, System.nanoTime());
211             enforceTimeout();
212         }
213     }
214 
enforceTimeout()215     private void enforceTimeout() {
216         if (mTimeoutActive) {
217             if (System.currentTimeMillis() - mCurrentSession.getStartTimestampMillis()
218                     > TIMEOUT_MILLIS) {
219                 onSessionEnd(Session.UNKNOWN);
220                 if (DEBUG) {
221                     Log.i(TAG, "Analytics timed out.");
222                 }
223             }
224         }
225     }
226 
227     @Override
onAccuracyChanged(Sensor sensor, int accuracy)228     public void onAccuracyChanged(Sensor sensor, int accuracy) {
229     }
230 
231     /**
232      * @return true if data is being collected - either for data gathering or creating a
233      *         rejected touch report.
234      */
isEnabled()235     public boolean isEnabled() {
236         return mEnableCollector || mAllowReportRejectedTouch;
237     }
238 
239     /**
240      * @return true if the full data set for data gathering should be collected - including
241      *         extensive sensor data, which is is not normally included with rejected touch reports.
242      */
isEnabledFull()243     public boolean isEnabledFull() {
244         return mEnableCollector;
245     }
246 
onScreenTurningOn()247     public void onScreenTurningOn() {
248         if (sessionEntrypoint()) {
249             if (DEBUG) {
250                 Log.d(TAG, "onScreenTurningOn");
251             }
252             addEvent(PhoneEvent.ON_SCREEN_ON);
253         }
254     }
255 
onScreenOnFromTouch()256     public void onScreenOnFromTouch() {
257         if (sessionEntrypoint()) {
258             if (DEBUG) {
259                 Log.d(TAG, "onScreenOnFromTouch");
260             }
261             addEvent(PhoneEvent.ON_SCREEN_ON_FROM_TOUCH);
262         }
263     }
264 
onScreenOff()265     public void onScreenOff() {
266         if (DEBUG) {
267             Log.d(TAG, "onScreenOff");
268         }
269         addEvent(PhoneEvent.ON_SCREEN_OFF);
270         sessionExitpoint(Session.FAILURE);
271     }
272 
onSucccessfulUnlock()273     public void onSucccessfulUnlock() {
274         if (DEBUG) {
275             Log.d(TAG, "onSuccessfulUnlock");
276         }
277         addEvent(PhoneEvent.ON_SUCCESSFUL_UNLOCK);
278         sessionExitpoint(Session.SUCCESS);
279     }
280 
onBouncerShown()281     public void onBouncerShown() {
282         if (DEBUG) {
283             Log.d(TAG, "onBouncerShown");
284         }
285         addEvent(PhoneEvent.ON_BOUNCER_SHOWN);
286     }
287 
onBouncerHidden()288     public void onBouncerHidden() {
289         if (DEBUG) {
290             Log.d(TAG, "onBouncerHidden");
291         }
292         addEvent(PhoneEvent.ON_BOUNCER_HIDDEN);
293     }
294 
onQsDown()295     public void onQsDown() {
296         if (DEBUG) {
297             Log.d(TAG, "onQsDown");
298         }
299         addEvent(PhoneEvent.ON_QS_DOWN);
300     }
301 
setQsExpanded(boolean expanded)302     public void setQsExpanded(boolean expanded) {
303         if (DEBUG) {
304             Log.d(TAG, "setQsExpanded = " + expanded);
305         }
306         if (expanded) {
307             addEvent(PhoneEvent.SET_QS_EXPANDED_TRUE);
308         } else {
309             addEvent(PhoneEvent.SET_QS_EXPANDED_FALSE);
310         }
311     }
312 
onTrackingStarted()313     public void onTrackingStarted() {
314         if (DEBUG) {
315             Log.d(TAG, "onTrackingStarted");
316         }
317         mTrackingStarted = true;
318         addEvent(PhoneEvent.ON_TRACKING_STARTED);
319     }
320 
onTrackingStopped()321     public void onTrackingStopped() {
322         if (mTrackingStarted) {
323             if (DEBUG) {
324                 Log.d(TAG, "onTrackingStopped");
325             }
326             mTrackingStarted = false;
327             addEvent(PhoneEvent.ON_TRACKING_STOPPED);
328         }
329     }
330 
onNotificationActive()331     public void onNotificationActive() {
332         if (DEBUG) {
333             Log.d(TAG, "onNotificationActive");
334         }
335         addEvent(PhoneEvent.ON_NOTIFICATION_ACTIVE);
336     }
337 
338 
onNotificationDoubleTap()339     public void onNotificationDoubleTap() {
340         if (DEBUG) {
341             Log.d(TAG, "onNotificationDoubleTap");
342         }
343         addEvent(PhoneEvent.ON_NOTIFICATION_DOUBLE_TAP);
344     }
345 
setNotificationExpanded()346     public void setNotificationExpanded() {
347         if (DEBUG) {
348             Log.d(TAG, "setNotificationExpanded");
349         }
350         addEvent(PhoneEvent.SET_NOTIFICATION_EXPANDED);
351     }
352 
onNotificatonStartDraggingDown()353     public void onNotificatonStartDraggingDown() {
354         if (DEBUG) {
355             Log.d(TAG, "onNotificationStartDraggingDown");
356         }
357         addEvent(PhoneEvent.ON_NOTIFICATION_START_DRAGGING_DOWN);
358     }
359 
onNotificatonStopDraggingDown()360     public void onNotificatonStopDraggingDown() {
361         if (DEBUG) {
362             Log.d(TAG, "onNotificationStopDraggingDown");
363         }
364         addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DRAGGING_DOWN);
365     }
366 
onNotificationDismissed()367     public void onNotificationDismissed() {
368         if (DEBUG) {
369             Log.d(TAG, "onNotificationDismissed");
370         }
371         addEvent(PhoneEvent.ON_NOTIFICATION_DISMISSED);
372     }
373 
onNotificatonStartDismissing()374     public void onNotificatonStartDismissing() {
375         if (DEBUG) {
376             Log.d(TAG, "onNotificationStartDismissing");
377         }
378         addEvent(PhoneEvent.ON_NOTIFICATION_START_DISMISSING);
379     }
380 
onNotificatonStopDismissing()381     public void onNotificatonStopDismissing() {
382         if (DEBUG) {
383             Log.d(TAG, "onNotificationStopDismissing");
384         }
385         addEvent(PhoneEvent.ON_NOTIFICATION_STOP_DISMISSING);
386     }
387 
onCameraOn()388     public void onCameraOn() {
389         if (DEBUG) {
390             Log.d(TAG, "onCameraOn");
391         }
392         addEvent(PhoneEvent.ON_CAMERA_ON);
393     }
394 
onLeftAffordanceOn()395     public void onLeftAffordanceOn() {
396         if (DEBUG) {
397             Log.d(TAG, "onLeftAffordanceOn");
398         }
399         addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_ON);
400     }
401 
onAffordanceSwipingStarted(boolean rightCorner)402     public void onAffordanceSwipingStarted(boolean rightCorner) {
403         if (DEBUG) {
404             Log.d(TAG, "onAffordanceSwipingStarted");
405         }
406         mCornerSwiping = true;
407         if (rightCorner) {
408             addEvent(PhoneEvent.ON_RIGHT_AFFORDANCE_SWIPING_STARTED);
409         } else {
410             addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_SWIPING_STARTED);
411         }
412     }
413 
onAffordanceSwipingAborted()414     public void onAffordanceSwipingAborted() {
415         if (mCornerSwiping) {
416             if (DEBUG) {
417                 Log.d(TAG, "onAffordanceSwipingAborted");
418             }
419             mCornerSwiping = false;
420             addEvent(PhoneEvent.ON_AFFORDANCE_SWIPING_ABORTED);
421         }
422     }
423 
onUnlockHintStarted()424     public void onUnlockHintStarted() {
425         if (DEBUG) {
426             Log.d(TAG, "onUnlockHintStarted");
427         }
428         addEvent(PhoneEvent.ON_UNLOCK_HINT_STARTED);
429     }
430 
onCameraHintStarted()431     public void onCameraHintStarted() {
432         if (DEBUG) {
433             Log.d(TAG, "onCameraHintStarted");
434         }
435         addEvent(PhoneEvent.ON_CAMERA_HINT_STARTED);
436     }
437 
onLeftAffordanceHintStarted()438     public void onLeftAffordanceHintStarted() {
439         if (DEBUG) {
440             Log.d(TAG, "onLeftAffordanceHintStarted");
441         }
442         addEvent(PhoneEvent.ON_LEFT_AFFORDANCE_HINT_STARTED);
443     }
444 
onTouchEvent(MotionEvent event, int width, int height)445     public void onTouchEvent(MotionEvent event, int width, int height) {
446         if (mCurrentSession != null) {
447             if (DEBUG) {
448                 Log.v(TAG, "onTouchEvent(ev.action="
449                         + MotionEvent.actionToString(event.getAction()) + ")");
450             }
451             mCurrentSession.addMotionEvent(event);
452             mCurrentSession.setTouchArea(width, height);
453             enforceTimeout();
454         }
455     }
456 
addEvent(int eventType)457     private void addEvent(int eventType) {
458         if (isEnabled() && mCurrentSession != null) {
459             mCurrentSession.addPhoneEvent(eventType, System.nanoTime());
460         }
461     }
462 
isReportingEnabled()463     public boolean isReportingEnabled() {
464         return mAllowReportRejectedTouch;
465     }
466 
onFalsingSessionStarted()467     public void onFalsingSessionStarted() {
468         sessionEntrypoint();
469     }
470 }
471