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